Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
030f6b8
Add assets & gradient extension
kvld Jun 5, 2018
be37415
Add achievements block to profile
kvld Jun 6, 2018
823849d
Add badges to achievements block
kvld Jun 6, 2018
e95b20a
Entity & API classes
kvld Jun 7, 2018
7f9fbbe
Add AchievementsRetriever
kvld Jun 8, 2018
20d274b
Working achievements in profile
kvld Jun 12, 2018
6f74184
Badge for locked achievement
kvld Jun 13, 2018
7c90d1c
Working achievements list
kvld Jun 13, 2018
ed8cd12
Add gradient & fix layout bugs
kvld Jun 14, 2018
ed056ae
Fix padding
kvld Jun 14, 2018
c158f01
Add skeleton view
kvld Jun 14, 2018
3aa4311
Add profile placeholders
kvld Jun 14, 2018
d612057
Add L10n
kvld Jun 14, 2018
ab3f61e
Merge branch 'dev' into feature/achievements
kvld Jun 14, 2018
9874e34
Update vector images
kvld Jun 14, 2018
000aa9d
Merge branch 'dev' into feature/achievements
Ostrenkiy Jun 14, 2018
3bca862
Achievements list
kvld Jun 18, 2018
49439a5
Add popup
kvld Jun 19, 2018
2ae3d95
Merge branch 'feature/achievements' of https://github.com/StepicOrg/s…
kvld Jun 19, 2018
13822a5
Use dynamic layout
kvld Jun 19, 2018
1701f91
Add banner view
kvld Jun 20, 2018
cb9f1c1
Fix banner
kvld Jun 20, 2018
88c3a4e
Open achievements in profile
kvld Jun 20, 2018
62c8cbc
Fix achievements order
kvld Jun 21, 2018
0c04f89
Remove frame log
kvld Jun 21, 2018
30876ca
Add sharing
kvld Jun 21, 2018
24245bc
Fix share text
kvld Jun 22, 2018
3d09703
Extract all progresses and all achievements loading
kvld Jun 22, 2018
33fef9c
Add skeletons
kvld Jun 26, 2018
07b6756
Add refresh after connection error
kvld Jun 26, 2018
ae041c7
Fix popup
kvld Jun 26, 2018
549e396
Merge branch 'dev' into feature/achievements
kvld Jun 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 111 additions & 3 deletions Stepic.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions Stepic/Achievement.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Achievement.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 06.06.2018.
// Copyright © 2018 Alex Karpov. All rights reserved.
//
import SwiftyJSON

class Achievement: JSONSerializable {
var id: Int
var kind: String
var iconImageUrl: String
var targetScore: Int

required init(json: JSON) {
self.id = json["id"].intValue
self.kind = json["kind"].stringValue
self.iconImageUrl = json["icon"].stringValue
self.targetScore = json["target_score"].intValue
}

func update(json: JSON) {
self.id = json["id"].intValue
self.kind = json["kind"].stringValue
self.iconImageUrl = json["icon"].stringValue
self.targetScore = json["target_score"].intValue
}
}
202 changes: 202 additions & 0 deletions Stepic/AchievementBadgeView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
//
// AchievementBadgeView.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 04.06.2018.
// Copyright © 2018 Vladislav Kiryukhin. All rights reserved.
//

import UIKit

struct AchievementBadgeViewData {
static var empty: AchievementBadgeViewData {
return AchievementBadgeViewData(completedLevel: 0, maxLevel: 0, stageProgress: 0.0, badge: #imageLiteral(resourceName: "achievement-0"))
}

let completedLevel: Int
let maxLevel: Int
let stageProgress: Float
let badge: UIImage
}

class AchievementBadgeView: UIView {
// Gradient colors and locations for progress circle
private static let colors = [
UIColor(hex: 0xa9aeff),
UIColor(hex: 0xa99cff),
UIColor(hex: 0xa992ff),
UIColor(hex: 0xaca5ff),
UIColor(hex: 0xacecfe)
]
private static let locations = [0.0, 0.14, 0.25, 0.425, 1.0]

private static let relativeBadgeHeight: CGFloat = 0.83
private static let relativeStarsHeight: CGFloat = 0.09
private static let relativeProgressWidth: CGFloat = 0.022
private static let relativeBadgeImagePadding: CGFloat = 0.03

@IBOutlet weak var paddingConstraint: NSLayoutConstraint!
@IBOutlet weak var circleViewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var starsStackViewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var badgeImageViewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var starsStackView: UIStackView!
@IBOutlet weak var circleView: UIView!
@IBOutlet weak var badgeImageView: UIImageView!

var data: AchievementBadgeViewData? {
didSet {
updateProgress()
}
}

var circleViewGradientLayer: CAGradientLayer?
var circleProgressLayer: CAShapeLayer?
private var previousBadgeFrame: CGRect?

override func awakeFromNib() {
super.awakeFromNib()

clipsToBounds = true

addGradient()
}

private func addGradient() {
circleViewGradientLayer = CAGradientLayer(colors: AchievementBadgeView.colors, locations: AchievementBadgeView.locations, rotationAngle: 130.0)

guard let circleViewGradientLayer = circleViewGradientLayer else {
return
}

circleViewGradientLayer.opacity = 0.25
circleView.layer.insertSublayer(circleViewGradientLayer, at: 0)
}

private func initViews() {
// Auto-resize: we calculate subviews sizes based on view height
let height = self.frame.height
let relativePaddingHeight = 1.0 - AchievementBadgeView.relativeBadgeHeight - AchievementBadgeView.relativeStarsHeight
let badgeHeight = AchievementBadgeView.relativeBadgeHeight * height
paddingConstraint.constant = relativePaddingHeight * height
circleViewHeightConstraint.constant = badgeHeight
starsStackViewHeightConstraint.constant = AchievementBadgeView.relativeStarsHeight * height
badgeImageViewHeightConstraint.constant = -2.0 * AchievementBadgeView.relativeBadgeImagePadding * badgeHeight
layoutIfNeeded()

let progressWidth = height * AchievementBadgeView.relativeProgressWidth
let innerCircleRadius = badgeHeight * 0.5

// Draw gradient circle
let gradientCircleLayer = CAShapeLayer()
gradientCircleLayer.lineWidth = progressWidth
let bezierPath = UIBezierPath()

bezierPath.addArc(withCenter: CGPoint(x: circleView.bounds.midX, y: circleView.bounds.midY), radius: innerCircleRadius - progressWidth, startAngle: 0, endAngle: 2 * .pi, clockwise: false)
gradientCircleLayer.path = bezierPath.cgPath
gradientCircleLayer.fillColor = nil
gradientCircleLayer.strokeColor = UIColor.black.cgColor

circleViewGradientLayer?.mask = gradientCircleLayer
}

private func initStageProgress(value: Float) {
let stageProgress = max(0.0, min(value, 1.0))

let height = self.frame.height
let badgeHeight = AchievementBadgeView.relativeBadgeHeight * height
let progressWidth = height * AchievementBadgeView.relativeProgressWidth

let innerCircleRadius = badgeHeight * 0.5
let progress = CGFloat(stageProgress) * 2.0 * .pi

let circlePath = UIBezierPath()
circlePath.addArc(withCenter: CGPoint(x: circleView.bounds.midX, y: circleView.bounds.midY), radius: innerCircleRadius - progressWidth, startAngle: .pi / 2, endAngle: .pi / 2 + progress, clockwise: true)

circleProgressLayer?.removeFromSuperlayer()
circleProgressLayer = CAShapeLayer()

guard let circleProgressLayer = circleProgressLayer else {
return
}

circleProgressLayer.path = circlePath.cgPath
circleProgressLayer.fillColor = nil
circleProgressLayer.strokeColor = UIColor.stepicGreen.cgColor
circleProgressLayer.lineWidth = progressWidth

circleView.layer.addSublayer(circleProgressLayer)
}

private func initLevelProgress(completedLevel: Int, maxLevel: Int) {
let completedLevel = max(min(maxLevel, completedLevel), 0)

for v in starsStackView.arrangedSubviews {
starsStackView.removeArrangedSubview(v)
v.removeFromSuperview()
}

if completedLevel != 0 {
// Remove previous width constraint (cause it based on maxLevel)
for c in starsStackView.constraints {
if c.firstAttribute == .width {
starsStackView.removeConstraint(c)
}
}

let filledCount = completedLevel
let borderedCount = completedLevel == maxLevel ? 0 : 1
let grayCount = maxLevel - filledCount - borderedCount

let spaceBetweenStars = starsStackViewHeightConstraint.constant * 0.3
starsStackView.spacing = spaceBetweenStars

NSLayoutConstraint(item: starsStackView, attribute: .width, relatedBy: .equal, toItem: starsStackView, attribute: .height, multiplier: CGFloat(maxLevel), constant: CGFloat(maxLevel - 1) * spaceBetweenStars).isActive = true

for _ in 0..<filledCount {
starsStackView.addArrangedSubview(UIImageView(image: #imageLiteral(resourceName: "star-filled")))
}

for _ in 0..<borderedCount {
starsStackView.addArrangedSubview(UIImageView(image: #imageLiteral(resourceName: "star-bordered")))
}

for _ in 0..<grayCount {
starsStackView.addArrangedSubview(UIImageView(image: #imageLiteral(resourceName: "star-gray")))
}
}
}

private func updateProgress() {
if let data = data {
if data.completedLevel == 0 {
circleViewGradientLayer?.isHidden = true
circleProgressLayer?.isHidden = true
} else {
circleViewGradientLayer?.isHidden = false
circleProgressLayer?.isHidden = false

initStageProgress(value: data.stageProgress)
}

badgeImageView.image = data.badge
initLevelProgress(completedLevel: data.completedLevel, maxLevel: data.maxLevel)
}
}

override func layoutSubviews() {
super.layoutSubviews()

if previousBadgeFrame != self.bounds {
if self.bounds.width != self.bounds.height {
print("achievement badge view: target size is not square, content would be clipped!")
}

previousBadgeFrame = self.bounds

circleViewGradientLayer?.frame = self.bounds
initViews()

updateProgress()
}
}
}
68 changes: 68 additions & 0 deletions Stepic/AchievementBadgeView.xib
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14109" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="AchievementBadgeView" customModule="Stepic" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="226" height="204"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="itK-AC-ShO">
<rect key="frame" x="113" y="0.0" width="0.0" height="0.0"/>
<subviews>
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="N3E-L0-3JB">
<constraints>
<constraint firstAttribute="width" secondItem="N3E-L0-3JB" secondAttribute="height" multiplier="1:1" id="PUy-Ig-lLA"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="width" secondItem="itK-AC-ShO" secondAttribute="height" multiplier="1:1" id="OYZ-dz-hem"/>
<constraint firstItem="N3E-L0-3JB" firstAttribute="width" secondItem="itK-AC-ShO" secondAttribute="width" id="OkG-4d-VcY"/>
<constraint firstItem="N3E-L0-3JB" firstAttribute="centerX" secondItem="itK-AC-ShO" secondAttribute="centerX" id="pW9-Pa-CL1"/>
<constraint firstItem="N3E-L0-3JB" firstAttribute="centerY" secondItem="itK-AC-ShO" secondAttribute="centerY" id="soe-fx-Xij"/>
<constraint firstAttribute="height" id="xPy-eG-dsZ"/>
</constraints>
</view>
<stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="sQW-MV-Bkj">
<rect key="frame" x="112.5" y="23" width="1" height="0.0"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" id="Fnu-Nx-gyG"/>
<constraint firstAttribute="width" constant="1" id="lZz-FH-VZf"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="sQW-MV-Bkj" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="iN0-l3-epB" secondAttribute="leading" priority="999" constant="8" id="KZN-x7-poH"/>
<constraint firstItem="sQW-MV-Bkj" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="RyI-I3-CHB"/>
<constraint firstItem="sQW-MV-Bkj" firstAttribute="top" secondItem="itK-AC-ShO" secondAttribute="bottom" constant="23" id="ccL-fa-bxp"/>
<constraint firstItem="itK-AC-ShO" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="mIP-O0-Skb"/>
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="sQW-MV-Bkj" secondAttribute="trailing" priority="999" constant="8" id="n0L-ZK-0XO"/>
<constraint firstItem="itK-AC-ShO" firstAttribute="centerX" secondItem="iN0-l3-epB" secondAttribute="centerX" id="xb4-dr-GbO"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<connections>
<outlet property="badgeImageView" destination="N3E-L0-3JB" id="Ngu-uE-X4h"/>
<outlet property="badgeImageViewHeightConstraint" destination="OkG-4d-VcY" id="3F7-xG-RRh"/>
<outlet property="circleView" destination="itK-AC-ShO" id="4ef-ZV-gpZ"/>
<outlet property="circleViewHeightConstraint" destination="xPy-eG-dsZ" id="Knt-6L-Iq1"/>
<outlet property="paddingConstraint" destination="ccL-fa-bxp" id="Z7D-jH-hJv"/>
<outlet property="starsStackView" destination="sQW-MV-Bkj" id="Fqv-ex-OsY"/>
<outlet property="starsStackViewHeightConstraint" destination="Fnu-Nx-gyG" id="uob-7F-1mQ"/>
</connections>
<point key="canvasLocation" x="99" y="-180"/>
</view>
</objects>
</document>
87 changes: 87 additions & 0 deletions Stepic/AchievementDescription.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// AchievementDescription.swift
// Stepic
//
// Created by Vladislav Kiryukhin on 12.06.2018.
// Copyright © 2018 Alex Karpov. All rights reserved.
//

import Foundation

enum AchievementKind: String {
// Cases should be declared in correct order
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А что от этого зависит сейчас?

case stepsSolved = "steps_solved"
case stepsSolvedStreak = "steps_solved_streak"
case stepsSolvedChoice = "steps_solved_choice"
case stepsSolvedCode = "steps_solved_code"
case stepsSolvedNumber = "steps_solved_number"
case codeQuizzesSolvedPython = "code_quizzes_solved_python"
case codeQuizzesSolvedCPP = "code_quizzes_solved_cpp"
case codeQuizzesSolvedJava = "code_quizzes_solved_java"
case activeDaysStreak = "active_days_streak"
case certificatesRegularCount = "certificates_regular_count"
case certificatesDistinctionCount = "certificates_distinction_count"
case courseReviewsCount = "course_reviews_count"

func getBadge(for level: Int) -> UIImage? {
return UIImage(named: "achievement-\(self.hashValue + 1)-\(level)")
}

func getName() -> String {
switch self {
case .stepsSolved:
return NSLocalizedString("AchievementsStepsSolvedKindTitle", comment: "")
case .stepsSolvedChoice:
return NSLocalizedString("AchievementsStepsSolvedChoiceKindTitle", comment: "")
case .stepsSolvedCode:
return NSLocalizedString("AchievementsStepsSolvedCodeKindTitle", comment: "")
case .stepsSolvedNumber:
return NSLocalizedString("AchievementsStepsSolvedNumberKindTitle", comment: "")
case .codeQuizzesSolvedPython:
return NSLocalizedString("AchievementsCodeQuizzesSolvedPythonKindTitle", comment: "")
case .codeQuizzesSolvedJava:
return NSLocalizedString("AchievementsCodeQuizzesSolvedJavaKindTitle", comment: "")
case .codeQuizzesSolvedCPP:
return NSLocalizedString("AchievementsCodeQuizzesSolvedCppKindTitle", comment: "")
case .certificatesRegularCount:
return NSLocalizedString("AchievementsCertificatesRegularCountKindTitle", comment: "")
case .certificatesDistinctionCount:
return NSLocalizedString("AchievementsCertificatesDistinctionCountKindTitle", comment: "")
case .courseReviewsCount:
return NSLocalizedString("AchievementsCourseReviewsCountKindTitle", comment: "")
case .stepsSolvedStreak:
return NSLocalizedString("AchievementsStepsSolvedStreakKindTitle", comment: "")
case .activeDaysStreak:
return NSLocalizedString("AchievementsActiveDaysStreakKindTitle", comment: "")
}
}

func getDescription(for score: Int) -> String {
switch self {
case .stepsSolved:
return String(format: NSLocalizedString("AchievementsStepsSolvedKindDescription", comment: ""), "\(score)")
case .stepsSolvedChoice:
return String(format: NSLocalizedString("AchievementsStepsSolvedChoiceKindDescription", comment: ""), "\(score)")
case .stepsSolvedCode:
return String(format: NSLocalizedString("AchievementsStepsSolvedCodeKindDescription", comment: ""), "\(score)")
case .stepsSolvedNumber:
return String(format: NSLocalizedString("AchievementsStepsSolvedNumberKindDescription", comment: ""), "\(score)")
case .codeQuizzesSolvedPython:
return String(format: NSLocalizedString("AchievementsCodeQuizzesSolvedPythonKindDescription", comment: ""), "\(score)")
case .codeQuizzesSolvedJava:
return String(format: NSLocalizedString("AchievementsCodeQuizzesSolvedJavaKindDescription", comment: ""), "\(score)")
case .codeQuizzesSolvedCPP:
return String(format: NSLocalizedString("AchievementsCodeQuizzesSolvedCppKindDescription", comment: ""), "\(score)")
case .certificatesRegularCount:
return String(format: NSLocalizedString("AchievementsCertificatesRegularCountKindDescription", comment: ""), "\(score)")
case .certificatesDistinctionCount:
return String(format: NSLocalizedString("AchievementsCertificatesDistinctionCountKindDescription", comment: ""), "\(score)")
case .courseReviewsCount:
return String(format: NSLocalizedString("AchievementsCourseReviewsCountKindDescription", comment: ""), "\(score)")
case .stepsSolvedStreak:
return String(format: NSLocalizedString("AchievementsStepsSolvedStreakKindDescription", comment: ""), "\(score)")
case .activeDaysStreak:
return String(format: NSLocalizedString("AchievementsActiveDaysStreakKindDescription", comment: ""), "\(score)")
}
}
}
Loading