From f36e07dfdbd161e484145e293a5e3d36b964af32 Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Fri, 30 Mar 2018 20:14:56 +0300 Subject: [PATCH 01/14] Fix TeamCity builds (#263) * Remove simulator clearing before tests * Fix Kanna builds * Fix Kanna builds * Set Kanna version * Force repo-update * Add run phase again * Run script only when installing --- Podfile | 2 +- Stepic.xcodeproj/project.pbxproj | 4 ++-- fastlane/Fastfile | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Podfile b/Podfile index b48a00f9c7..7967fa3672 100644 --- a/Podfile +++ b/Podfile @@ -37,7 +37,7 @@ def all_pods pod 'BEMCheckBox' pod 'IQKeyboardManagerSwift' - pod 'Kanna', :git => 'https://github.com/tid-kijyun/Kanna.git', :branch => 'master' + pod 'Kanna', '~> 4.0.0' pod 'CRToast', :git => 'https://github.com/cruffenach/CRToast.git', :branch => 'master' pod 'TUSafariActivity', '~> 1.0' diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 80c31c34be..68c070c212 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -10239,14 +10239,14 @@ }; 2C47A16C206297D0003E87EC /* ShellScript */ = { isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 8; files = ( ); inputPaths = ( ); outputPaths = ( ); - runOnlyForDeploymentPostprocessing = 0; + runOnlyForDeploymentPostprocessing = 1; shellPath = /bin/sh; shellScript = "/usr/bin/xcrun simctl uninstall booted com.AlexKarpov.Stepic\n\n"; }; diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 19ef6dd48a..4f6d8d9673 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -18,7 +18,7 @@ default_platform :ios platform :ios do before_all do - cocoapods + cocoapods(repo_update: true) end def version_string(version_number, build_number) From af12c03e4f945a2e1524923880e2b538ef7916bc Mon Sep 17 00:00:00 2001 From: Alexander Karpov Date: Sun, 1 Apr 2018 13:37:25 +0300 Subject: [PATCH 02/14] Fixed bad tooltip positioning (#260) --- Stepic/HomeScreenViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Stepic/HomeScreenViewController.swift b/Stepic/HomeScreenViewController.swift index f7b6892cec..c21a8e45a8 100644 --- a/Stepic/HomeScreenViewController.swift +++ b/Stepic/HomeScreenViewController.swift @@ -167,7 +167,7 @@ class HomeScreenViewController: UIViewController, HomeScreenView { return } strongSelf.continueLearningTooltip = TooltipFactory.continueLearningWidget - strongSelf.continueLearningTooltip?.show(direction: .up, in: nil, from: strongSelf.continueLearningWidget.continueLearningButton) + strongSelf.continueLearningTooltip?.show(direction: .up, in: strongSelf.continueLearningWidget, from: strongSelf.continueLearningWidget.continueLearningButton) TooltipDefaultsManager.shared.didShowOnHomeContinueLearning = true strongSelf.viewWillAppearBlock = nil } From f5d8cb36b220cdd078cdc1396b65bfd150370474 Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Mon, 2 Apr 2018 21:09:11 +0300 Subject: [PATCH 03/14] Fix webview bugs in adaptive steps (#259) --- Stepic/CardStepViewController.swift | 51 ++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/Stepic/CardStepViewController.swift b/Stepic/CardStepViewController.swift index 7bfff95ae1..b96128782f 100644 --- a/Stepic/CardStepViewController.swift +++ b/Stepic/CardStepViewController.swift @@ -23,6 +23,9 @@ class CardStepViewController: UIViewController, CardStepView { @IBOutlet weak var quizPlaceholderView: UIView! var stepWebViewHeight: NSLayoutConstraint! + // For updates after rotation only when controller not presented + var shouldRefreshOnAppear: Bool = false + var baseScrollView: UIScrollView { get { return scrollView @@ -42,14 +45,8 @@ class CardStepViewController: UIViewController, CardStepView { } @objc func didScreenRotate() { - alignImages(in: stepWebView).then { - self.getContentHeight(self.stepWebView) - }.then { height -> Void in - self.resetWebViewHeight(Float(height)) - self.scrollView.layoutIfNeeded() - }.catch { _ in - print("card step: error after rotation") - } + refreshWebView() + shouldRefreshOnAppear = !shouldRefreshOnAppear } override func viewWillAppear(_ animated: Bool) { @@ -57,6 +54,15 @@ class CardStepViewController: UIViewController, CardStepView { view.setNeedsLayout() view.layoutIfNeeded() + + if shouldRefreshOnAppear { + refreshWebView() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + shouldRefreshOnAppear = false } deinit { @@ -85,6 +91,7 @@ class CardStepViewController: UIViewController, CardStepView { scrollView.insertSubview(stepWebView, at: 0) stepWebViewHeight = stepWebView.constrainHeight("5") + stepWebView.translatesAutoresizingMaskIntoConstraints = false stepWebView.constrainBottomSpace(toView: quizPlaceholderView, predicate: "0") stepWebView.alignLeadingEdge(withView: scrollView, predicate: "2") stepWebView.alignTrailingEdge(withView: scrollView, predicate: "-2") @@ -129,6 +136,34 @@ class CardStepViewController: UIViewController, CardStepView { } } } + + private func refreshWebView() { + resetWebViewHeight(5.0) + + func reloadContent() -> Promise { + return Promise { fulfill, reject in + self.stepWebView.evaluateJavaScript("location.reload();", completionHandler: { _, error in + if let error = error { + return reject(error) + } + + fulfill(()) + }) + } + } + + reloadContent().then { + self.alignImages(in: self.stepWebView) + }.then { + self.getContentHeight(self.stepWebView) + }.then { height -> Void in + self.resetWebViewHeight(Float(height)) + self.scrollView.layoutIfNeeded() + }.catch { _ in + print("card step: error while refreshing") + } + + } } extension CardStepViewController: WKNavigationDelegate { From cf88a206476e7b01d478cb15a61fc496348559f5 Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Mon, 2 Apr 2018 21:09:32 +0300 Subject: [PATCH 04/14] New autocomplete keywords (#258) * Add keywords * Move words for autocomplete to plist file --- Stepic.xcodeproj/project.pbxproj | 4 + Stepic/AutocompleteWords.swift | 773 +----------------------- Stepic/autocomplete_suggestions.plist | 821 ++++++++++++++++++++++++++ 3 files changed, 850 insertions(+), 748 deletions(-) create mode 100644 Stepic/autocomplete_suggestions.plist diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index 68c070c212..d79b36bade 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -1566,6 +1566,7 @@ 2C0AF18A203C606E000EA3B6 /* arrow_right.svg in Resources */ = {isa = PBXBuildFile; fileRef = 2CB62BEC2019FDB000B5E336 /* arrow_right.svg */; }; 2C0AF18C203C67EC000EA3B6 /* MigrationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C0AF18B203C67EC000EA3B6 /* MigrationExtensions.swift */; }; 2C1000802029BA7F00E55597 /* BaseCardsStepsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5D51582024653B00B9D932 /* BaseCardsStepsViewController.swift */; }; + 2C104B682069064D0026FEB9 /* autocomplete_suggestions.plist in Resources */ = {isa = PBXBuildFile; fileRef = 2C104B672069064D0026FEB9 /* autocomplete_suggestions.plist */; }; 2C1219901F9655AB00A43E98 /* NotificationsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C12198F1F9655AB00A43E98 /* NotificationsSection.swift */; }; 2C15EB961FC70A0300F56D93 /* UICollectionViewFlowLayout+PlusCrashWorkaround.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C15EB951FC70A0300F56D93 /* UICollectionViewFlowLayout+PlusCrashWorkaround.swift */; }; 2C1B60C01F4C4AEF00236804 /* CodeLanguagePickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 081BF3461EFEF630002F84AA /* CodeLanguagePickerViewController.swift */; }; @@ -4950,6 +4951,7 @@ 210D872D0B8EC9F0380D3EF9 /* Pods-Adaptive GMAT.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adaptive GMAT.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Adaptive GMAT/Pods-Adaptive GMAT.debug.xcconfig"; sourceTree = ""; }; 2C03B2A31F0CD87600005383 /* StringHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringHelper.swift; sourceTree = ""; }; 2C0AF18B203C67EC000EA3B6 /* MigrationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrationExtensions.swift; sourceTree = ""; }; + 2C104B672069064D0026FEB9 /* autocomplete_suggestions.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = autocomplete_suggestions.plist; sourceTree = ""; }; 2C12198F1F9655AB00A43E98 /* NotificationsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsSection.swift; sourceTree = ""; }; 2C15EB951FC70A0300F56D93 /* UICollectionViewFlowLayout+PlusCrashWorkaround.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionViewFlowLayout+PlusCrashWorkaround.swift"; sourceTree = ""; }; 2C1649661F2A0842002C9F99 /* AdaptiveAchievementsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveAchievementsViewController.swift; sourceTree = ""; }; @@ -6875,6 +6877,7 @@ 087D96B81EBB7C7700059408 /* Tokens.plist */, 087D96BA1EBB7F5500059408 /* Tokens.swift */, 2CBC4C051F1E4A1300FE96C4 /* Config.plist */, + 2C104B672069064D0026FEB9 /* autocomplete_suggestions.plist */, ); name = "Supporting Files"; sourceTree = ""; @@ -9053,6 +9056,7 @@ 2CB9E8C01F7AA5760004E17F /* Notifications.storyboard in Resources */, 080E81011F0146EF00DC0EA5 /* FullscreenCodeQuizViewController.xib in Resources */, 08A125791BDEBCC90066B2B2 /* Localizable.strings in Resources */, + 2C104B682069064D0026FEB9 /* autocomplete_suggestions.plist in Resources */, 08DF78A81F5DE66200AEEA85 /* ArtView.xib in Resources */, 2C7FEE7A1FDAFB4600B2B4F1 /* OnboardingPageView.xib in Resources */, 2C6E9CD51FED6ADD001821A2 /* loading_robot.gif in Resources */, diff --git a/Stepic/AutocompleteWords.swift b/Stepic/AutocompleteWords.swift index a4a5e52885..53ecbf2fb4 100644 --- a/Stepic/AutocompleteWords.swift +++ b/Stepic/AutocompleteWords.swift @@ -10,6 +10,8 @@ import Foundation struct AutocompleteWords { + private static let suggestionsFilename = "autocomplete_suggestions" + static func autocompleteFor(_ text: String, language: CodeLanguage) -> [String] { var suggestions: [String] = [] @@ -34,6 +36,12 @@ struct AutocompleteWords { break case .sql: suggestions = sql + case .haskell, .haskell7, .haskell8: + suggestions = haskell + case .r: + suggestions = r + case .kotlin: + suggestions = kotlin default: suggestions = [] break @@ -44,753 +52,22 @@ struct AutocompleteWords { } } - static let python = [ - "False", - "class", - "finally", - "is", - "return", - "None", - "continue", - "for", - "lambda", - "try", - "True", - "def", - "from", - "nonlocal", - "while", - "and", - "del", - "global", - "not", - "with", - "as", - "elif", - "if", - "or", - "yield", - "assert", - "else", - "import", - "pass", - "print", - "break", - "except", - "in", - "raise" - ] - - static let cpp = [ - "bool", - "break", - "case", - "catch", - "char", - "class", - "const", - "cout", - "cin", - "endl", - "include", - "iostream", - "continue", - "default", - "delete", - "do", - "double", - "else", - "enum", - "extern", - "false", - "float", - "for", - "friend", - "goto", - "if", - "inline", - "int", - "long", - "mutable", - "namespace", - "new", - "operator", - "private", - "protected", - "public", - "return", - "short", - "signed", - "sizeof", - "static", - "string", - "struct", - "switch", - "template", - "this", - "throw", - "true", - "try", - "typedef", - "typename", - "union", - "unsigned", - "using", - "virtual", - "void", - "while" - ] - - static let cs = [ - "abstract", - "base", - "bool", - "break", - "byte", - "case", - "catch", - "char", - "checked", - "class", - "const", - "continue", - "decimal", - "default", - "delegate", - "do", - "double", - "else", - "enum", - "event", - "explicit", - "extern", - "false", - "finally", - "fixed", - "float", - "for", - "foreach", - "goto", - "if", - "implicit", - "int", - "interface", - "internal", - "lock", - "long", - "namespace", - "new", - "null", - "object", - "operator", - "out", - "override", - "params", - "private", - "protected", - "public", - "readonly", - "ref", - "return", - "sbyte", - "sealed", - "short", - "sizeof", - "stackalloc", - "static", - "string", - "struct", - "switch", - "this", - "throw", - "true", - "try", - "typeof", - "uint", - "ulong", - "unchecked", - "unsafe", - "ushort", - "using", - "virtual", - "void", - "volatile", - "while", - "set", - "get", - "var" - ] - - static let java = [ - "abstract", - "assert", - "boolean", - "break", - "byte", - "case", - "catch", - "char", - "class", - "const", - "continue", - "default", - "do", - "double", - "else", - "enum", - "extends", - "final", - "finally", - "float", - "for", - "goto", - "if", - "implements", - "import", - "instanceof", - "int", - "interface", - "long", - "native", - "new", - "package", - "private", - "protected", - "public", - "return", - "short", - "static", - "super", - "switch", - "synchronized", - "this", - "throw", - "throws", - "try", - "void", - "volatile", - "while", - "false", - "null", - "true", - "System", - "out", - "print", - "println", - "main", - "String", - "Math", - "Scanner", - "Thread", - "ArrayList", - "LinkedList", - "HashMap", - "HashSet", - "Collections", - "Iterator", - "File", - "Formatter", - "Exception" - ] - - static let js = [ - "abstract", - "arguments", - "boolean", - "break", - "byte", - "case", - "catch", - "char", - "class", - "const", - "continue", - "debugger", - "default", - "delete", - "do", - "double", - "else", - "enum", - "eval", - "export", - "extends", - "false", - "final", - "finally", - "float", - "for", - "function", - "goto", - "if", - "implements", - "in", - "instanceof", - "int", - "interface", - "let", - "long", - "native", - "new", - "null", - "package", - "private", - "protected", - "public", - "return", - "short", - "static", - "super", - "switch", - "synchronized", - "this", - "throw", - "throws", - "transient", - "true", - "try", - "typeof", - "var", - "void", - "volatile", - "while", - "with", - "yield", - "Array", - "Date", - "length", - "Math", - "NaN", - "name", - "Number", - "Object", - "prototype", - "String", - "toString", - "undefinedvalueOf", - "alert", - "prompt", - "confirm" - ] - - static let ruby = [ - "and", - "begin", - "break", - "case", - "class", - "def", - "do", - "else", - "elsif", - "each", - "end", - "true", - "false", - "for", - "if", - "in", - "module", - "next", - "nil", - "not", - "or", - "rescue", - "retry", - "return", - "self", - "super", - "then", - "undef", - "unless", - "until", - "when", - "while", - "yield", - "attr_accessor", - "attr_reader", - "attr_writer", - "initialize", - "new", - "puts", - "gets", - "print", - "Struct", - "Math", - "Time", - "Proc", - "File", - "lambda", - "Comparable", - "Enumerable" - ] + private static func loadSuggestionsFromFile(language: String) -> [String] { + if let path = Bundle.main.path(forResource: suggestionsFilename, ofType: "plist"), + let words = NSDictionary(contentsOfFile: path) as? [String: [String]] { + return words[language] ?? [] + } + return [] + } - static let sql = [ - "ADD", - "ALL", - "ALTER", - "AND", - "ANY", - "APPLY", - "AS", - "ASC", - "AUTHORIZATION", - "BACKUP", - "BEGIN", - "BETWEEN", - "BREAK", - "BROWSE", - "BULK", - "BY", - "CASCADE", - "CASE", - "CHECK", - "CHECKPOINT", - "CLOSE", - "CLUSTERED", - "COALESCE", - "COLLATE", - "COLUMN", - "COMMIT", - "COMPUTE", - "CONNECT", - "CONSTRAINT", - "CONTAINS", - "CONTAINSTABLE", - "CONTINUE", - "CONVERT", - "CREATE", - "CROSS", - "CURRENT", - "CURRENT_DATE", - "CURRENT_TIME", - "CURRENT_TIMESTAMP", - "CURRENT_USER", - "CURSOR", - "DATABASE", - "DBCC", - "DEALLOCATE", - "DECLARE", - "DEFAULT", - "DELETE", - "DENY", - "DESC", - "DISK", - "DISTINCT", - "DISTRIBUTED", - "DOUBLE", - "DROP", - "DUMMY", - "DUMP", - "ELSE", - "END", - "ERRLVL", - "ESCAPE", - "EXCEPT", - "EXEC", - "EXECUTE", - "EXISTS", - "EXIT", - "FETCH", - "FILE", - "FILLFACTOR", - "FOLLOWING", - "FOR", - "FOREIGN", - "FREETEXT", - "FREETEXTTABLE", - "FROM", - "FULL", - "FUNCTION", - "GOTO", - "GRANT", - "GROUP", - "HAVING", - "HOLDLOCK", - "IDENTITY", - "IDENTITYCOL", - "IDENTITY_INSERT", - "IF", - "IN", - "INDEX", - "INNER", - "INSERT", - "INTERSECT", - "INTO", - "IS", - "JOIN", - "KEY", - "KILL", - "LEFT", - "LIKE", - "LINENO", - "LOAD", - "MATCH", - "MERGE", - "NATIONAL", - "NOCHECK", - "NONCLUSTERED", - "NOT", - "NULL", - "NULLIF", - "OF", - "OFF", - "OFFSETS", - "ON", - "OPEN", - "OPENDATASOURCE", - "OPENQUERY", - "OPENROWSET", - "OPENXML", - "OPTION", - "OR", - "ORDER", - "OUTER", - "OVER", - "PERCENT", - "PLAN", - "PRECEDING", - "PRECISION", - "PRIMARY", - "PRINT", - "PROC", - "PROCEDURE", - "PUBLIC", - "RAISERROR", - "READ", - "READTEXT", - "RECONFIGURE", - "REFERENCES", - "REPLICATION", - "RESTORE", - "RESTRICT", - "RETURN", - "REVOKE", - "RIGHT", - "ROLLBACK", - "ROWCOUNT", - "ROWGUIDCOL", - "ROWS?", - "RULE", - "SAVE", - "SCHEMA", - "SELECT", - "SESSION_USER", - "SET", - "SETUSER", - "SHUTDOWN", - "SOME", - "STATISTICS", - "SYSTEM_USER", - "TABLE", - "TEXTSIZE", - "THEN", - "TO", - "TOP", - "TRAN", - "TRANSACTION", - "TRIGGER", - "TRUNCATE", - "TSEQUAL", - "UNBOUNDED", - "UNION", - "UNIQUE", - "UPDATE", - "UPDATETEXT", - "USE", - "USER", - "USING", - "VALUES", - "VARYING", - "VIEW", - "WAITFOR", - "WHEN", - "WHERE", - "WHILE", - "WITH", - "WRITETEXT", - "add", - "all", - "alter", - "and", - "any", - "apply", - "as", - "asc", - "authorization", - "backup", - "begin", - "between", - "break", - "browse", - "bulk", - "by", - "cascade", - "case", - "check", - "checkpoint", - "close", - "clustered", - "coalesce", - "collate", - "column", - "commit", - "compute", - "connect", - "constraint", - "contains", - "containstable", - "continue", - "convert", - "create", - "cross", - "current", - "current_date", - "current_time", - "current_timestamp", - "current_user", - "cursor", - "database", - "dbcc", - "deallocate", - "declare", - "default", - "delete", - "deny", - "desc", - "disk", - "distinct", - "distributed", - "double", - "drop", - "dummy", - "dump", - "else", - "end", - "errlvl", - "escape", - "except", - "exec", - "execute", - "exists", - "exit", - "fetch", - "file", - "fillfactor", - "following", - "for", - "foreign", - "freetext", - "freetexttable", - "from", - "full", - "function", - "goto", - "grant", - "group", - "having", - "holdlock", - "identity", - "identitycol", - "identity_insert", - "if", - "in", - "index", - "inner", - "insert", - "intersect", - "into", - "is", - "join", - "key", - "kill", - "left", - "like", - "lineno", - "load", - "match", - "merge", - "national", - "nocheck", - "nonclustered", - "not", - "null", - "nullif", - "of", - "off", - "offsets", - "on", - "open", - "opendatasource", - "openquery", - "openrowset", - "openxml", - "option", - "or", - "order", - "outer", - "over", - "percent", - "plan", - "preceding", - "precision", - "primary", - "print", - "proc", - "procedure", - "public", - "raiserror", - "read", - "readtext", - "reconfigure", - "references", - "replication", - "restore", - "restrict", - "return", - "revoke", - "right", - "rollback", - "rowcount", - "rowguidcol", - "rows?", - "rule", - "save", - "schema", - "select", - "session_user", - "set", - "setuser", - "shutdown", - "some", - "statistics", - "system_user", - "table", - "textsize", - "then", - "to", - "top", - "tran", - "transaction", - "trigger", - "truncate", - "tsequal", - "unbounded", - "union", - "unique", - "update", - "updatetext", - "use", - "user", - "using", - "values", - "varying", - "view", - "waitfor", - "when", - "where", - "while", - "with", - "writetext" - ] + static let python = loadSuggestionsFromFile(language: "python") + static let cpp = loadSuggestionsFromFile(language: "cpp") + static let cs = loadSuggestionsFromFile(language: "cs") + static let java = loadSuggestionsFromFile(language: "java") + static let ruby = loadSuggestionsFromFile(language: "ruby") + static let sql = loadSuggestionsFromFile(language: "sql") + static let kotlin = loadSuggestionsFromFile(language: "kotlin") + static let js = loadSuggestionsFromFile(language: "js") + static let r = loadSuggestionsFromFile(language: "r") + static let haskell = loadSuggestionsFromFile(language: "haskell") } diff --git a/Stepic/autocomplete_suggestions.plist b/Stepic/autocomplete_suggestions.plist new file mode 100644 index 0000000000..36fc3ae73f --- /dev/null +++ b/Stepic/autocomplete_suggestions.plist @@ -0,0 +1,821 @@ + + + + + cpp + + bool + break + case + catch + char + class + const + cout + cin + endl + include + iostream + continue + default + delete + do + double + else + enum + extern + false + float + for + friend + goto + if + inline + int + long + mutable + namespace + new + operator + private + protected + public + return + short + signed + sizeof + static + string + struct + switch + template + this + throw + true + try + typedef + typename + union + unsigned + using + virtual + void + while + + haskell + + case + class + data + default + deriving + do + else + forall + if + import + in + infix + infixl + infixr + instance + let + module + newtype + of + qualified + then + type + where + _ + foreign + ccall + as + safe + unsafe + + cs + + abstract + base + bool + break + byte + case + catch + char + checked + class + const + continue + decimal + default + delegate + do + double + else + enum + event + explicit + extern + false + finally + fixed + float + for + foreach + goto + if + implicit + int + interface + internal + lock + long + namespace + new + null + object + operator + out + override + params + private + protected + public + readonly + ref + return + sbyte + sealed + short + sizeof + stackalloc + static + string + struct + switch + this + throw + true + try + typeof + uint + ulong + unchecked + unsafe + ushort + using + virtual + void + volatile + while + set + get + var + + java + + abstract + assert + boolean + break + byte + case + catch + char + class + const + continue + default + do + double + else + enum + extends + final + finally + float + for + goto + if + implements + import + instanceof + int + interface + long + native + new + package + private + protected + public + return + short + static + super + switch + synchronized + this + throw + throws + try + void + volatile + while + false + null + true + System + out + print + println + main + String + Math + Scanner + Thread + ArrayList + LinkedList + HashMap + HashSet + Collections + Iterator + File + Formatter + Exception + + ruby + + and + begin + break + case + class + def + do + else + elsif + each + end + true + false + for + if + in + module + next + nil + not + or + rescue + retry + return + self + super + then + undef + unless + until + when + while + yield + attr_accessor + attr_reader + attr_writer + initialize + new + puts + gets + print + Struct + Math + Time + Proc + File + lambda + Comparable + Enumerable + + sql + + ADD + ALL + ALTER + AND + ANY + APPLY + AS + ASC + AUTHORIZATION + BACKUP + BEGIN + BETWEEN + BREAK + BROWSE + BULK + BY + CASCADE + CASE + CHECK + CHECKPOINT + CLOSE + CLUSTERED + COALESCE + COLLATE + COLUMN + COMMIT + COMPUTE + CONNECT + CONSTRAINT + CONTAINS + CONTAINSTABLE + CONTINUE + CONVERT + CREATE + CROSS + CURRENT + CURRENT_DATE + CURRENT_TIME + CURRENT_TIMESTAMP + CURRENT_USER + CURSOR + DATABASE + DBCC + DEALLOCATE + DECLARE + DEFAULT + DELETE + DENY + DESC + DISK + DISTINCT + DISTRIBUTED + DOUBLE + DROP + DUMMY + DUMP + ELSE + END + ERRLVL + ESCAPE + EXCEPT + EXEC + EXECUTE + EXISTS + EXIT + FETCH + FILE + FILLFACTOR + FOLLOWING + FOR + FOREIGN + FREETEXT + FREETEXTTABLE + FROM + FULL + FUNCTION + GOTO + GRANT + GROUP + HAVING + HOLDLOCK + IDENTITY + IDENTITYCOL + IDENTITY_INSERT + IF + IN + INDEX + INNER + INSERT + INTERSECT + INTO + IS + JOIN + KEY + KILL + LEFT + LIKE + LINENO + LOAD + MATCH + MERGE + NATIONAL + NOCHECK + NONCLUSTERED + NOT + NULL + NULLIF + OF + OFF + OFFSETS + ON + OPEN + OPENDATASOURCE + OPENQUERY + OPENROWSET + OPENXML + OPTION + OR + ORDER + OUTER + OVER + PERCENT + PLAN + PRECEDING + PRECISION + PRIMARY + PRINT + PROC + PROCEDURE + PUBLIC + RAISERROR + READ + READTEXT + RECONFIGURE + REFERENCES + REPLICATION + RESTORE + RESTRICT + RETURN + REVOKE + RIGHT + ROLLBACK + ROWCOUNT + ROWGUIDCOL + ROWS? + RULE + SAVE + SCHEMA + SELECT + SESSION_USER + SET + SETUSER + SHUTDOWN + SOME + STATISTICS + SYSTEM_USER + TABLE + TEXTSIZE + THEN + TO + TOP + TRAN + TRANSACTION + TRIGGER + TRUNCATE + TSEQUAL + UNBOUNDED + UNION + UNIQUE + UPDATE + UPDATETEXT + USE + USER + USING + VALUES + VARYING + VIEW + WAITFOR + WHEN + WHERE + WHILE + WITH + WRITETEXT + add + all + alter + and + any + apply + as + asc + authorization + backup + begin + between + break + browse + bulk + by + cascade + case + check + checkpoint + close + clustered + coalesce + collate + column + commit + compute + connect + constraint + contains + containstable + continue + convert + create + cross + current + current_date + current_time + current_timestamp + current_user + cursor + database + dbcc + deallocate + declare + default + delete + deny + desc + disk + distinct + distributed + double + drop + dummy + dump + else + end + errlvl + escape + except + exec + execute + exists + exit + fetch + file + fillfactor + following + for + foreign + freetext + freetexttable + from + full + function + goto + grant + group + having + holdlock + identity + identitycol + identity_insert + if + in + index + inner + insert + intersect + into + is + join + key + kill + left + like + lineno + load + match + merge + national + nocheck + nonclustered + not + null + nullif + of + off + offsets + on + open + opendatasource + openquery + openrowset + openxml + option + or + order + outer + over + percent + plan + preceding + precision + primary + print + proc + procedure + public + raiserror + read + readtext + reconfigure + references + replication + restore + restrict + return + revoke + right + rollback + rowcount + rowguidcol + rows? + rule + save + schema + select + session_user + set + setuser + shutdown + some + statistics + system_user + table + textsize + then + to + top + tran + transaction + trigger + truncate + tsequal + unbounded + union + unique + update + updatetext + use + user + using + values + varying + view + waitfor + when + where + while + with + writetext + + kotlin + + package + import + typealias + class + interface + constructor + by + where + init + companion + object + val + var + fun + this + dynamic + if + try + catch + finally + do + while + true + false + in + !in + as + !as + is + !is + throw + return + continue + break + else + abstract + final + enum + open + annatation + sealed + data + override + lateinit + private + protected + public + internal + in + out + inline + noinline + crossinline + vararg + const + suspend + reified + null + + r + + if + else + repeat + while + function + for + in + next + break + TRUE + FALSE + NULL + Inf + NaN + NA + NA_integer_ + NA_real_ + NA_complex_ + NA_character_ + + js + + abstract + arguments + await + boolean + break + byte + case + catch + char + class + const + continue + debugger + default + delete + do + double + else + enum + eval + export + extends + false + final + finally + float + for + function + goto + if + implements + import + in + instanceof + int + interface + let + long + native + new + null + package + private + protected + public + return + short + static + super + switch + synchronized + this + throw + throws + transient + true + try + typeof + var + void + volatile + while + with + yield + + + From 583d669da115c26f82482e513795ede1140183ea Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Mon, 2 Apr 2018 21:14:32 +0300 Subject: [PATCH 05/14] Video in background (#261) * Play video in background * Add remote config * Forbid command center * Add tooltip * Revert manual signing in pbxproj --- Stepic/Info.plist | 8 ++- Stepic/Player.swift | 54 +++++++++++++------- Stepic/RemoteConfig.swift | 13 ++++- Stepic/StepicVideoPlayerViewController.swift | 37 +++++++++++++- Stepic/Tooltip.swift | 21 ++++++-- Stepic/TooltipDefaultsManager.swift | 15 ++++++ Stepic/TooltipFactory.swift | 4 ++ Stepic/en.lproj/Localizable.strings | 1 + Stepic/ru.lproj/Localizable.strings | 1 + 9 files changed, 129 insertions(+), 25 deletions(-) diff --git a/Stepic/Info.plist b/Stepic/Info.plist index 19a7f8dda2..f4950d3aef 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -2,8 +2,6 @@ - UIStatusBarStyle - UIStatusBarStyleLightContent CFBundleDevelopmentRegion en CFBundleExecutable @@ -89,6 +87,10 @@ NSAllowsArbitraryLoads + UIBackgroundModes + + audio + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -97,6 +99,8 @@ armv7 + UIStatusBarStyle + UIStatusBarStyleLightContent UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/Stepic/Player.swift b/Stepic/Player.swift index fbf62210ff..e27c9063e5 100644 --- a/Stepic/Player.swift +++ b/Stepic/Player.swift @@ -293,6 +293,15 @@ public class Player: UIViewController { public override func viewDidLoad() { super.viewDidLoad() + if RemoteConfig.shared.allowVideoInBackground { + do { + try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + print("failed to set up background playing: \(error)") + } + } + self.playerView.layer.addObserver(self, forKeyPath: PlayerReadyForDisplayKey, options: ([.new, .old]), context: &PlayerLayerObserverContext) self.timeObserver = self.avplayer.addPeriodicTimeObserver(forInterval: CMTimeMake(1, 100), queue: DispatchQueue.main, using: { [weak self] _ in guard let strongSelf = self else { return } @@ -468,28 +477,41 @@ extension Player { // UIApplication internal func addApplicationObservers() { - NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive(_:)), name: NSNotification.Name.UIApplicationWillResignActive, object: UIApplication.shared) - NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(_:)), name: NSNotification.Name.UIApplicationDidEnterBackground, object: UIApplication.shared) - NotificationCenter.default.addObserver(self, selector: #selector(applicationWillEnterForeground(_:)), name: NSNotification.Name.UIApplicationWillEnterForeground, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationWillResignActive(_:)), name: .UIApplicationWillResignActive, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidBecomeActive(_:)), name: .UIApplicationDidBecomeActive, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidEnterBackground(_:)), name: .UIApplicationDidEnterBackground, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationWillEnterForeground(_:)), name: .UIApplicationWillEnterForeground, object: UIApplication.shared) } internal func removeApplicationObservers() { } - @objc internal func applicationWillResignActive(_ aNotification: NSNotification) { - if self.playbackState == .playing { + @objc internal func handleApplicationWillResignActive(_ aNotification: Notification) { + if !RemoteConfig.shared.allowVideoInBackground && self.playbackState == .playing { self.pause() } } - @objc internal func applicationDidEnterBackground(_ aNotification: NSNotification) { - if self.playbackState == .playing { - self.pause() + @objc internal func handleApplicationDidBecomeActive(_ aNotification: Notification) { + if RemoteConfig.shared.allowVideoInBackground { + // Attach AVPlayer to AVPlayerLayer again + playerView.player = self.avplayer + } + } + + @objc internal func handleApplicationDidEnterBackground(_ aNotification: Notification) { + if RemoteConfig.shared.allowVideoInBackground { + // Detach AVPlayer from AVPlayerLayer (from Apple's manual) + playerView.player = nil + } else { + if self.playbackState == .playing { + self.pause() + } } } - @objc internal func applicationWillEnterForeground(_ aNoticiation: NSNotification) { - if self.playbackState == .paused { + @objc internal func handleApplicationWillEnterForeground(_ aNoticiation: Notification) { + if !RemoteConfig.shared.allowVideoInBackground && self.playbackState == .paused { self.playFromCurrentTime() } } @@ -622,21 +644,17 @@ extension Player { internal class PlayerView: UIView { - var player: AVPlayer! { + var player: AVPlayer? { get { - return (self.layer as! AVPlayerLayer).player + return playerLayer.player } set { - if (self.layer as! AVPlayerLayer).player != newValue { - (self.layer as! AVPlayerLayer).player = newValue - } + playerLayer.player = newValue } } var playerLayer: AVPlayerLayer { - get { - return self.layer as! AVPlayerLayer - } + return layer as! AVPlayerLayer } var fillMode: String { diff --git a/Stepic/RemoteConfig.swift b/Stepic/RemoteConfig.swift index a7c77f7865..7d8536b65e 100644 --- a/Stepic/RemoteConfig.swift +++ b/Stepic/RemoteConfig.swift @@ -13,10 +13,12 @@ enum RemoteConfigKeys: String { case showStreaksNotificationTrigger = "show_streaks_notification_trigger" case adaptiveBackendUrl = "adaptive_backend_url" case supportedInAdaptiveModeCourses = "adaptive_courses_ios" + case allowVideoInBackground = "allow_video_in_background" } class RemoteConfig { private let defaultShowStreaksNotificationTrigger = ShowStreaksNotificationTrigger.loginAndSubmission + private let defaultAllowVideoInBackground = false static let shared = RemoteConfig() var loadingDoneCallback: (() -> Void)? @@ -25,7 +27,8 @@ class RemoteConfig { lazy var appDefaults: [String: NSObject] = [ RemoteConfigKeys.showStreaksNotificationTrigger.rawValue: defaultShowStreaksNotificationTrigger.rawValue as NSObject, RemoteConfigKeys.adaptiveBackendUrl.rawValue: StepicApplicationsInfo.adaptiveRatingURL as NSObject, - RemoteConfigKeys.supportedInAdaptiveModeCourses.rawValue: StepicApplicationsInfo.adaptiveSupportedCourses as NSObject + RemoteConfigKeys.supportedInAdaptiveModeCourses.rawValue: StepicApplicationsInfo.adaptiveSupportedCourses as NSObject, + RemoteConfigKeys.allowVideoInBackground.rawValue: defaultAllowVideoInBackground as NSObject ] enum ShowStreaksNotificationTrigger: String { @@ -57,6 +60,14 @@ class RemoteConfig { return ids } + var allowVideoInBackground: Bool { + guard let configValue = FirebaseRemoteConfig.RemoteConfig.remoteConfig().configValue(forKey: RemoteConfigKeys.allowVideoInBackground.rawValue).stringValue else { + return defaultAllowVideoInBackground + } + + return configValue == "true" + } + init() { loadDefaultValues() fetchCloudValues() diff --git a/Stepic/StepicVideoPlayerViewController.swift b/Stepic/StepicVideoPlayerViewController.swift index ec3463b4ea..c0b5378b80 100644 --- a/Stepic/StepicVideoPlayerViewController.swift +++ b/Stepic/StepicVideoPlayerViewController.swift @@ -230,6 +230,7 @@ class StepicVideoPlayerViewController: UIViewController { fileprivate var player: Player! var video: Video! + var videoInBackgroundTooltip: Tooltip? override func viewDidLoad() { super.viewDidLoad() @@ -238,6 +239,8 @@ class StepicVideoPlayerViewController: UIViewController { WatchSessionSender.sendPlaybackStatus(.available) NotificationCenter.default.addObserver(self, selector: #selector(StepicVideoPlayerViewController.audioRouteChanged(_:)), name: NSNotification.Name.AVAudioSessionRouteChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidBecomeActive(_:)), name: .UIApplicationDidBecomeActive, object: UIApplication.shared) + NotificationCenter.default.addObserver(self, selector: #selector(handleApplicationDidEnterBackground(_:)), name: .UIApplicationDidEnterBackground, object: UIApplication.shared) topTimeSlider.setThumbImage(Images.playerControls.timeSliderThumb, for: UIControlState()) @@ -275,7 +278,34 @@ class StepicVideoPlayerViewController: UIViewController { topTimeSlider.addTarget(self, action: #selector(StepicVideoPlayerViewController.finishedSeeking), for: UIControlEvents.touchUpInside) topTimeSlider.addTarget(self, action: #selector(StepicVideoPlayerViewController.startedSeeking), for: UIControlEvents.touchDown) MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(self, action: #selector(StepicVideoPlayerViewController.togglePlayPause)) -// MPRemoteCommandCenter.sharedCommandCenter().togglePlayPauseCommand.addTarget(self, action: #selector(togglePlayStop(_:))); + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if RemoteConfig.shared.allowVideoInBackground && TooltipDefaultsManager.shared.shouldShowInVideoPlayer { + delay(2.0) { [weak self] in + guard let s = self else { + return + } + + s.videoInBackgroundTooltip = TooltipFactory.videoInBackground + s.videoInBackgroundTooltip?.show(direction: .down, in: s.view, from: s.fullscreenPlayButton, isArrowVisible: false) + TooltipDefaultsManager.shared.didShowInVideoPlayer = true + } + } + } + + @objc internal func handleApplicationDidEnterBackground(_ aNotification: Notification) { + if !RemoteConfig.shared.allowVideoInBackground { + MPRemoteCommandCenter.shared().togglePlayPauseCommand.removeTarget(self) + } + } + + @objc internal func handleApplicationDidBecomeActive(_ aNotification: Notification) { + if !RemoteConfig.shared.allowVideoInBackground { + MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget(self, action: #selector(StepicVideoPlayerViewController.togglePlayPause)) + } } @objc func togglePlayPause() { @@ -389,9 +419,14 @@ class StepicVideoPlayerViewController: UIViewController { extension StepicVideoPlayerViewController : PlayerDelegate { func playerReady(_ player: Player) { + guard player.playbackState == .stopped else { + return + } + print("player is ready to display") activityIndicator.isHidden = true setTimeParametersAfterPlayerIsReady() + player.seekToTime(CMTime(seconds: playerStartTime, preferredTimescale: 1000)) player.playFromCurrentTime() player.rate = currentRate.rawValue diff --git a/Stepic/Tooltip.swift b/Stepic/Tooltip.swift index 85c8e8f18d..69e9e0ce75 100644 --- a/Stepic/Tooltip.swift +++ b/Stepic/Tooltip.swift @@ -12,6 +12,7 @@ import EasyTipView protocol Tooltip { init(text: String, shouldDismissAfterTime: Bool, color: TooltipColor) func show(direction: TooltipDirection, in inView: UIView?, from fromView: UIView) + func show(direction: TooltipDirection, in inView: UIView?, from fromView: UIView, isArrowVisible: Bool) func show(direction: TooltipDirection, in inView: UIView?, from fromItem: UIBarButtonItem) func dismiss() } @@ -85,8 +86,18 @@ class EasyTipTooltip: Tooltip { preferences.drawing.borderColor = color.borderColor } - private func setupTooltip(direction: TooltipDirection) { + private func setupTooltip(direction: TooltipDirection, isArrowVisible: Bool) { preferences.drawing.arrowPosition = easyTipDirectionFromTooltipDirection(direction: direction) + + if !isArrowVisible { + switch direction { + case .up, .down: + preferences.drawing.arrowWidth = CGFloat(0) + case .left, .right: + preferences.drawing.arrowHeight = CGFloat(0) + } + } + easyTip = EasyTipView(text: text, preferences: preferences, delegate: nil) } @@ -101,13 +112,17 @@ class EasyTipTooltip: Tooltip { } func show(direction: TooltipDirection, in inView: UIView?, from fromView: UIView) { - setupTooltip(direction: direction) + show(direction: direction, in: inView, from: fromView, isArrowVisible: true) + } + + func show(direction: TooltipDirection, in inView: UIView?, from fromView: UIView, isArrowVisible: Bool) { + setupTooltip(direction: direction, isArrowVisible: isArrowVisible) easyTip.show(forView: fromView, withinSuperview: inView) setupDisappear() } func show(direction: TooltipDirection, in inView: UIView?, from fromItem: UIBarButtonItem) { - setupTooltip(direction: direction) + setupTooltip(direction: direction, isArrowVisible: true) easyTip.show(forItem: fromItem, withinSuperView: inView) setupDisappear() } diff --git a/Stepic/TooltipDefaultsManager.swift b/Stepic/TooltipDefaultsManager.swift index 50ae4637d6..a4178a90e9 100644 --- a/Stepic/TooltipDefaultsManager.swift +++ b/Stepic/TooltipDefaultsManager.swift @@ -17,6 +17,7 @@ class TooltipDefaultsManager { private let didShowOnLessonDownloadsKey = "didShowOnLessonDownloadsKey" private let didShowOnHomeContinueLearningKey = "didShowOnHomeContinueLearningKey" private let didShowOnStreaksSwitchInProfileKey = "didShowOnStreaksSwitchInProfileKey" + private let didShowInVideoPlayerKey = "didShowInVideoPlayerKey" var didShowOnLessonDownloads: Bool { set(value) { @@ -48,6 +49,16 @@ class TooltipDefaultsManager { } } + var didShowInVideoPlayer: Bool { + set(value) { + defaults.set(value, forKey: didShowInVideoPlayerKey) + } + + get { + return defaults.value(forKey: didShowInVideoPlayerKey) as? Bool ?? false + } + } + var shouldShowOnHomeContinueLearning: Bool { return !didShowOnHomeContinueLearning } @@ -59,4 +70,8 @@ class TooltipDefaultsManager { var shouldShowOnStreaksSwitchInProfile: Bool { return !didShowOnStreaksSwitchInProfile } + + var shouldShowInVideoPlayer: Bool { + return !didShowInVideoPlayer + } } diff --git a/Stepic/TooltipFactory.swift b/Stepic/TooltipFactory.swift index 8dfb75a6ea..9833e5fd42 100644 --- a/Stepic/TooltipFactory.swift +++ b/Stepic/TooltipFactory.swift @@ -24,4 +24,8 @@ struct TooltipFactory { static var streaksTooltip: Tooltip { return EasyTipTooltip(text: NSLocalizedString("StreaksSwitchTooltip", comment: ""), shouldDismissAfterTime: true, color: .standard) } + + static var videoInBackground: Tooltip { + return EasyTipTooltip(text: NSLocalizedString("VideoInBackgroundTooltip", comment: ""), shouldDismissAfterTime: true, color: .standard) + } } diff --git a/Stepic/en.lproj/Localizable.strings b/Stepic/en.lproj/Localizable.strings index 34d38bf252..4a6767473f 100644 --- a/Stepic/en.lproj/Localizable.strings +++ b/Stepic/en.lproj/Localizable.strings @@ -400,6 +400,7 @@ ShareCourseTooltip = "Share the link with your friends to learn together"; LessonDownloadTooltip = "Download lesson to watch lectures offline"; ContinueLearningWidgetTooltip = "Tap to continue from where you finished last time"; StreaksSwitchTooltip = "Turn on to get new portion of knowledge every day"; +VideoInBackgroundTooltip = "You can play video in background"; /* Notification alerts */ NotificationTabNotificationRequestAlertTitle = "Stay tuned"; diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index cc0ae68a5e..8e61647fa6 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -401,6 +401,7 @@ ShareCourseTooltip = "Поделитесь ссылкой с друзьями, LessonDownloadTooltip = "Загрузите урок, чтобы смотреть видео оффлайн"; ContinueLearningWidgetTooltip = "Нажмите, чтобы перейти к тому месту, где закончили в прошлый раз"; StreaksSwitchTooltip = "Включите, чтобы получать новую порцию знаний каждый день"; +VideoInBackgroundTooltip = "Вы можете продолжить воспроизведение видео в фоновом режиме"; /* Notification alerts */ NotificationTabNotificationRequestAlertTitle = "Следи за обновлениями"; From e12221bd3fed707fbab778b0b849540c8a36696d Mon Sep 17 00:00:00 2001 From: Alexander Karpov Date: Tue, 3 Apr 2018 13:14:50 +0300 Subject: [PATCH 06/14] Network layer refactoring (#262) * Added APIEndpoint inheritance to all network classes * working implementation * working adapter * Updated JSONSerializable & added generic update() method * More update refactoring * Added generic delete request * Added create() method * Create comment reworked * Added views create() method * minor * refactored create() for attempts and submissions * Added retrieve() for one object by id * Added retrieve with parameters * minor * Added retrieve() with fetch to NotificationsAPI * minor * Refactored UnitsAPI * multiple fixes * Refactored UserActivitiesAPI * Refactor DiscussionProxiesAPI * Refactored getObjectsByIds * Refactored CommentsAPI * Added deprecations to CommentsAPI * Added default implementation of async fetch using DatabaseFetchService * fixed GET requests & added deprecations to CoursesAPI * Refactored CertificatesAPI * Refactored CourseListsAPI * Refactored NotificationsStatusesAPI * fixed formatting * Removed some checkToken() calls * Fixed JSONSerializables * AsyncFetchService -> DatabaseFetchService * Cleaned up UserActivitiesAPI * minor format * fixed force unwrap * Removed forced unwraps * Renamed IdType & some more * Added CoreDataRepresentable protocol for IdType of IDFetchable * fixed "Submit" localization * Deleted not passing tests file * minor --- Stepic.xcodeproj/project.pbxproj | 116 ++++++----- .../xcschemes/xcschememanagement.plist | 7 +- Stepic/APIEndpoint.swift | 141 +++----------- Stepic/AdaptiveRatingsAPI.swift | 1 + Stepic/ApiRequestRetrier.swift | 34 ++++ Stepic/AppDelegate.swift | 4 +- Stepic/Assignment.swift | 9 +- Stepic/AssignmentsAPI.swift | 2 +- Stepic/Attempt.swift | 41 +++- Stepic/AttemptsAPI.swift | 69 +++---- Stepic/AuthAPI.swift | 2 +- Stepic/AuthInfo.swift | 2 +- Stepic/Certificate.swift | 22 ++- Stepic/CertificatesAPI.swift | 74 ++----- Stepic/Comment.swift | 83 +++----- Stepic/CommentsAPI.swift | 120 ++++-------- Stepic/Course.swift | 8 +- Stepic/CourseList.swift | 8 +- Stepic/CourseListPresenter.swift | 2 - Stepic/CourseListsAPI.swift | 35 +--- Stepic/CourseReviewSummariesAPI.swift | 2 +- Stepic/CourseReviewSummary.swift | 8 +- Stepic/CoursesAPI.swift | 123 ++---------- Stepic/CreateRequestMaker.swift | 57 ++++++ Stepic/DatabaseFetchService.swift | 40 ++++ Stepic/DeleteRequestMaker.swift | 31 +++ Stepic/DevicesAPI.swift | 4 + Stepic/DiscussionProxiesAPI.swift | 50 ++--- Stepic/DiscussionProxy.swift | 11 +- Stepic/EnrollmentsAPI.swift | 9 +- Stepic/ExplorePresenter.swift | 15 +- Stepic/IDFetchable.swift | 49 +++++ Stepic/JSONInitializable.swift | 22 --- Stepic/JSONSerializable.swift | 40 ++++ Stepic/LastStep.swift | 9 +- Stepic/LastStepsAPI.swift | 3 +- Stepic/Lesson.swift | 8 +- Stepic/LessonsAPI.swift | 2 +- .../contents | 2 +- Stepic/Notification.swift | 15 +- Stepic/NotificationRegistrator.swift | 1 + Stepic/NotificationStatusesAPI.swift | 17 +- Stepic/NotificationsAPI.swift | 83 +++----- Stepic/NotificationsPresenter.swift | 16 +- Stepic/NotificationsStatus.swift | 15 +- Stepic/Profile.swift | 13 +- Stepic/ProfilesAPI.swift | 21 +- Stepic/Progress.swift | 20 +- Stepic/ProgressesAPI.swift | 2 +- Stepic/QueriesAPI.swift | 6 +- Stepic/QuizPresenter.swift | 11 +- Stepic/RecommendationsAPI.swift | 1 + Stepic/RetrieveRequestMaker.swift | 180 ++++++++++++++++++ Stepic/Section.swift | 8 +- Stepic/SectionsAPI.swift | 2 +- Stepic/Sorter.swift | 2 +- Stepic/Step.swift | 8 +- Stepic/StepicsAPI.swift | 12 +- Stepic/StepikModelView.swift | 39 ++++ Stepic/StepsAPI.swift | 2 +- Stepic/StepsControllerDeepLinkRouter.swift | 4 +- Stepic/StepsControllerRouter.swift | 176 ----------------- Stepic/Submission.swift | 45 ++++- Stepic/SubmissionsAPI.swift | 64 +++---- Stepic/Unit.swift | 8 +- Stepic/UnitsAPI.swift | 88 ++++----- Stepic/UpdateRequestMaker.swift | 36 ++++ Stepic/User.swift | 21 +- Stepic/UserActivitiesAPI.swift | 72 ++----- Stepic/UserActivity.swift | 10 +- Stepic/UsersAPI.swift | 2 +- Stepic/Video.swift | 9 +- Stepic/ViewsAPI.swift | 5 + Stepic/Vote.swift | 28 ++- Stepic/VotesAPI.swift | 44 ++--- Stepic/WriteCommentViewController.swift | 2 +- Stepic/ru.lproj/Localizable.strings | 1 - StepicTests/CertificatesAPITest.swift | 71 ------- StepicTests/CookieTests.swift | 4 +- 79 files changed, 1136 insertions(+), 1293 deletions(-) create mode 100644 Stepic/ApiRequestRetrier.swift create mode 100644 Stepic/CreateRequestMaker.swift create mode 100644 Stepic/DatabaseFetchService.swift create mode 100644 Stepic/DeleteRequestMaker.swift create mode 100644 Stepic/IDFetchable.swift delete mode 100644 Stepic/JSONInitializable.swift create mode 100644 Stepic/JSONSerializable.swift create mode 100644 Stepic/RetrieveRequestMaker.swift create mode 100644 Stepic/StepikModelView.swift delete mode 100644 Stepic/StepsControllerRouter.swift create mode 100644 Stepic/UpdateRequestMaker.swift delete mode 100644 StepicTests/CertificatesAPITest.swift diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index d79b36bade..b00287aba4 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -192,7 +192,7 @@ 0828FF7D1BC7F9D7000AFEA7 /* Unit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF7B1BC7F9D6000AFEA7 /* Unit.swift */; }; 0828FF801BC7FD24000AFEA7 /* Lesson+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF7E1BC7FD24000AFEA7 /* Lesson+CoreDataProperties.swift */; }; 0828FF811BC7FD24000AFEA7 /* Lesson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF7F1BC7FD24000AFEA7 /* Lesson.swift */; }; - 0828FF831BC800C0000AFEA7 /* JSONInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */; }; + 0828FF831BC800C0000AFEA7 /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */; }; 0828FF861BC81EEC000AFEA7 /* UnitsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF851BC81EEC000AFEA7 /* UnitsViewController.swift */; }; 0828FF8C1BC81F41000AFEA7 /* UnitTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF8A1BC81F41000AFEA7 /* UnitTableViewCell.swift */; }; 0828FF8D1BC81F41000AFEA7 /* UnitTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0828FF8B1BC81F41000AFEA7 /* UnitTableViewCell.xib */; }; @@ -221,8 +221,6 @@ 082E5E0E1F46379100F41426 /* ReplyCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082E5E0D1F46379100F41426 /* ReplyCache.swift */; }; 082E5E0F1F46379100F41426 /* ReplyCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082E5E0D1F46379100F41426 /* ReplyCache.swift */; }; 082E5E101F46379100F41426 /* ReplyCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082E5E0D1F46379100F41426 /* ReplyCache.swift */; }; - 082EDED31D881562006B51DC /* StepsControllerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082EDED21D881562006B51DC /* StepsControllerRouter.swift */; }; - 082EDED41D881562006B51DC /* StepsControllerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082EDED21D881562006B51DC /* StepsControllerRouter.swift */; }; 082FD64C1D6C849C007F3E07 /* ReplaceLastSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082FD64B1D6C849C007F3E07 /* ReplaceLastSegue.swift */; }; 082FD64D1D6C849C007F3E07 /* ReplaceLastSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082FD64B1D6C849C007F3E07 /* ReplaceLastSegue.swift */; }; 083056D21DBFB4BE00F1F2A4 /* DiscussionWebTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B9770A1D19D5AA00FFC52C /* DiscussionWebTableViewCell.swift */; }; @@ -271,7 +269,7 @@ 083D64AF1C19BDB2003222F0 /* ControllerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083D64AE1C19BDB2003222F0 /* ControllerHelper.swift */; }; 083E1DA61C96E9F100B305E4 /* ApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083E1DA51C96E9F100B305E4 /* ApplicationInfo.swift */; }; 083E1DA71C96E9F100B305E4 /* ApplicationInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083E1DA51C96E9F100B305E4 /* ApplicationInfo.swift */; }; - 083F2B0E1E9D87C000714173 /* CertificatesAPITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083F2B0D1E9D87C000714173 /* CertificatesAPITest.swift */; }; + 083E49DD2072B684004896C0 /* IDFetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083E49DC2072B684004896C0 /* IDFetchable.swift */; }; 083F2B111E9D8E8F00714173 /* CertificatesStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 083F2B101E9D8E8F00714173 /* CertificatesStoryboard.storyboard */; }; 083F2B121E9D8E8F00714173 /* CertificatesStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 083F2B101E9D8E8F00714173 /* CertificatesStoryboard.storyboard */; }; 083F2B141E9D8EF800714173 /* CertificatesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083F2B131E9D8EF800714173 /* CertificatesViewController.swift */; }; @@ -452,6 +450,7 @@ 0861E6761CD8106E00B45652 /* ExecutionQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0861E6741CD8106E00B45652 /* ExecutionQueue.swift */; }; 0861E67B1CD9483500B45652 /* ExecutionQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0861E67A1CD9483500B45652 /* ExecutionQueues.swift */; }; 0861E67C1CD9483500B45652 /* ExecutionQueues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0861E67A1CD9483500B45652 /* ExecutionQueues.swift */; }; + 0863B9672069A41A0023A182 /* RetrieveRequestMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0863B9662069A41A0023A182 /* RetrieveRequestMaker.swift */; }; 086442B51F764F67000CC53D /* AuthNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D674921D78B46900B60963 /* AuthNavigationViewController.swift */; }; 086442B61F764F68000CC53D /* AuthNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D674921D78B46900B60963 /* AuthNavigationViewController.swift */; }; 086442B81F764F69000CC53D /* AuthNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D674921D78B46900B60963 /* AuthNavigationViewController.swift */; }; @@ -671,6 +670,12 @@ 088E58C21DE34E2F0009B9CE /* SocialSDKProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E58C01DE34E2F0009B9CE /* SocialSDKProvider.swift */; }; 088E58C41DE34ED20009B9CE /* VKSocialSDKProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E58C31DE34ED20009B9CE /* VKSocialSDKProvider.swift */; }; 088E58C51DE34ED20009B9CE /* VKSocialSDKProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E58C31DE34ED20009B9CE /* VKSocialSDKProvider.swift */; }; + 088E73EA2060124B00D458E3 /* ApiRequestRetrier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E73E92060124B00D458E3 /* ApiRequestRetrier.swift */; }; + 088E73EB206014B000D458E3 /* AdaptiveRatingsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA9D9842010EEA2007AA743 /* AdaptiveRatingsAPI.swift */; }; + 088E73ED20614DFC00D458E3 /* UpdateRequestMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E73EC20614DFC00D458E3 /* UpdateRequestMaker.swift */; }; + 088E73F020619C8F00D458E3 /* DeleteRequestMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E73EF20619C8F00D458E3 /* DeleteRequestMaker.swift */; }; + 088E73F220619EEF00D458E3 /* CreateRequestMaker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E73F120619EEF00D458E3 /* CreateRequestMaker.swift */; }; + 088E73F42061BDAA00D458E3 /* StepikModelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088E73F32061BDAA00D458E3 /* StepikModelView.swift */; }; 088EDB501F8E8A5D009B736E /* CourseListVerticalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088EDB4F1F8E8A5D009B736E /* CourseListVerticalViewController.swift */; }; 088EDB511F8E8A5D009B736E /* CourseListVerticalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088EDB4F1F8E8A5D009B736E /* CourseListVerticalViewController.swift */; }; 088EDB521F8E8A5D009B736E /* CourseListVerticalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 088EDB4F1F8E8A5D009B736E /* CourseListVerticalViewController.swift */; }; @@ -1193,7 +1198,7 @@ 08D120E81C937B2200A54ABC /* AuthInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0885F8571BAAD43300F2A188 /* AuthInfo.swift */; }; 08D120E91C937B2200A54ABC /* VideoDownloadDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089B370C1BFA07AE003DC593 /* VideoDownloadDelegate.swift */; }; 08D120EA1C937B2200A54ABC /* TextReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F485991C57868E000165AA /* TextReply.swift */; }; - 08D120EB1C937B2200A54ABC /* JSONInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */; }; + 08D120EB1C937B2200A54ABC /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */; }; 08D120EC1C937B2200A54ABC /* UIImageViewExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CA59F31BC020E2008DC44D /* UIImageViewExtension.swift */; }; 08D120ED1C937B2200A54ABC /* Lesson+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF7E1BC7FD24000AFEA7 /* Lesson+CoreDataProperties.swift */; }; 08D120EE1C937B2200A54ABC /* VideoDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086E965F1BF66A1800AB952D /* VideoDownloadView.swift */; }; @@ -1260,6 +1265,7 @@ 08D5F5851F7DBA70007C1634 /* CourseReviewSummariesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D5F57F1F7DBA70007C1634 /* CourseReviewSummariesAPI.swift */; }; 08D5F5861F7DBA70007C1634 /* CourseReviewSummariesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D5F57F1F7DBA70007C1634 /* CourseReviewSummariesAPI.swift */; }; 08D5F5871F7DBA70007C1634 /* CourseReviewSummariesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D5F57F1F7DBA70007C1634 /* CourseReviewSummariesAPI.swift */; }; + 08D9E98C206C243D002F41D3 /* DatabaseFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D9E98B206C243D002F41D3 /* DatabaseFetchService.swift */; }; 08DA79011DB6BB36003491C4 /* ControllerHelperLaunchExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DA79001DB6BB36003491C4 /* ControllerHelperLaunchExtension.swift */; }; 08DB8CB91D0BECF000A6D079 /* DiscussionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DB8CB71D0BECF000A6D079 /* DiscussionTableViewCell.swift */; }; 08DB8CBA1D0BECF000A6D079 /* DiscussionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DB8CB71D0BECF000A6D079 /* DiscussionTableViewCell.swift */; }; @@ -1877,14 +1883,13 @@ 2C1B621B1F4C4AEF00236804 /* StepicsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB82341D74926F00FDEADE /* StepicsAPI.swift */; }; 2C1B621C1F4C4AEF00236804 /* UnitsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF21D89C86F00B27C95 /* UnitsAPI.swift */; }; 2C1B621D1F4C4AEF00236804 /* UserActivitiesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB671DC8DF59006622EC /* UserActivitiesAPI.swift */; }; - 2C1B62201F4C4AEF00236804 /* JSONInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */; }; + 2C1B62201F4C4AEF00236804 /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */; }; 2C1B62211F4C4AEF00236804 /* TabsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74601BD8159F0064AAEA /* TabsInfo.swift */; }; 2C1B62221F4C4AEF00236804 /* VideosInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74631BD9213D0064AAEA /* VideosInfo.swift */; }; 2C1B62231F4C4AEF00236804 /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0857AA201BECDA640044B505 /* VideoDownload.swift */; }; 2C1B62241F4C4AEF00236804 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080DCF161C4518BC00DE3E2E /* SearchResult.swift */; }; 2C1B62251F4C4AEF00236804 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AEC3A41CCA69F300FFF29E /* Device.swift */; }; 2C1B62261F4C4AEF00236804 /* CyrillicURLActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084070861D64DC7500308FC1 /* CyrillicURLActivityItemSource.swift */; }; - 2C1B62271F4C4AEF00236804 /* StepsControllerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082EDED21D881562006B51DC /* StepsControllerRouter.swift */; }; 2C1B62281F4C4AEF00236804 /* StepsControllerDeepLinkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF51D8C835600B27C95 /* StepsControllerDeepLinkRouter.swift */; }; 2C1B62291F4C4AEF00236804 /* UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB6A1DC8EB45006622EC /* UserActivity.swift */; }; 2C1B622A1F4C4AEF00236804 /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083749251DE5AE0400144C14 /* Alerts.swift */; }; @@ -2282,14 +2287,13 @@ 2C1B642C1F4C590700236804 /* StepicsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB82341D74926F00FDEADE /* StepicsAPI.swift */; }; 2C1B642D1F4C590700236804 /* UnitsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF21D89C86F00B27C95 /* UnitsAPI.swift */; }; 2C1B642E1F4C590700236804 /* UserActivitiesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB671DC8DF59006622EC /* UserActivitiesAPI.swift */; }; - 2C1B64311F4C590700236804 /* JSONInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */; }; + 2C1B64311F4C590700236804 /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */; }; 2C1B64321F4C590700236804 /* TabsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74601BD8159F0064AAEA /* TabsInfo.swift */; }; 2C1B64331F4C590700236804 /* VideosInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74631BD9213D0064AAEA /* VideosInfo.swift */; }; 2C1B64341F4C590700236804 /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0857AA201BECDA640044B505 /* VideoDownload.swift */; }; 2C1B64351F4C590700236804 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080DCF161C4518BC00DE3E2E /* SearchResult.swift */; }; 2C1B64361F4C590700236804 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AEC3A41CCA69F300FFF29E /* Device.swift */; }; 2C1B64371F4C590700236804 /* CyrillicURLActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084070861D64DC7500308FC1 /* CyrillicURLActivityItemSource.swift */; }; - 2C1B64381F4C590700236804 /* StepsControllerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082EDED21D881562006B51DC /* StepsControllerRouter.swift */; }; 2C1B64391F4C590700236804 /* StepsControllerDeepLinkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF51D8C835600B27C95 /* StepsControllerDeepLinkRouter.swift */; }; 2C1B643A1F4C590700236804 /* UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB6A1DC8EB45006622EC /* UserActivity.swift */; }; 2C1B643B1F4C590700236804 /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083749251DE5AE0400144C14 /* Alerts.swift */; }; @@ -3031,14 +3035,13 @@ 2C89AAAF1F4C289900227C3B /* StepicsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB82341D74926F00FDEADE /* StepicsAPI.swift */; }; 2C89AAB01F4C289900227C3B /* UnitsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF21D89C86F00B27C95 /* UnitsAPI.swift */; }; 2C89AAB11F4C289900227C3B /* UserActivitiesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB671DC8DF59006622EC /* UserActivitiesAPI.swift */; }; - 2C89AAB41F4C289900227C3B /* JSONInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */; }; + 2C89AAB41F4C289900227C3B /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */; }; 2C89AAB51F4C289900227C3B /* TabsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74601BD8159F0064AAEA /* TabsInfo.swift */; }; 2C89AAB61F4C289900227C3B /* VideosInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74631BD9213D0064AAEA /* VideosInfo.swift */; }; 2C89AAB71F4C289900227C3B /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0857AA201BECDA640044B505 /* VideoDownload.swift */; }; 2C89AAB81F4C289900227C3B /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080DCF161C4518BC00DE3E2E /* SearchResult.swift */; }; 2C89AAB91F4C289900227C3B /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AEC3A41CCA69F300FFF29E /* Device.swift */; }; 2C89AABA1F4C289900227C3B /* CyrillicURLActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084070861D64DC7500308FC1 /* CyrillicURLActivityItemSource.swift */; }; - 2C89AABB1F4C289900227C3B /* StepsControllerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082EDED21D881562006B51DC /* StepsControllerRouter.swift */; }; 2C89AABC1F4C289900227C3B /* StepsControllerDeepLinkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF51D8C835600B27C95 /* StepsControllerDeepLinkRouter.swift */; }; 2C89AABD1F4C289900227C3B /* UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB6A1DC8EB45006622EC /* UserActivity.swift */; }; 2C89AABE1F4C289900227C3B /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083749251DE5AE0400144C14 /* Alerts.swift */; }; @@ -3442,14 +3445,13 @@ 2C9732631F4C38F600AC9301 /* StepicsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB82341D74926F00FDEADE /* StepicsAPI.swift */; }; 2C9732641F4C38F600AC9301 /* UnitsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF21D89C86F00B27C95 /* UnitsAPI.swift */; }; 2C9732651F4C38F600AC9301 /* UserActivitiesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB671DC8DF59006622EC /* UserActivitiesAPI.swift */; }; - 2C9732681F4C38F600AC9301 /* JSONInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */; }; + 2C9732681F4C38F600AC9301 /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */; }; 2C9732691F4C38F600AC9301 /* TabsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74601BD8159F0064AAEA /* TabsInfo.swift */; }; 2C97326A1F4C38F600AC9301 /* VideosInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74631BD9213D0064AAEA /* VideosInfo.swift */; }; 2C97326B1F4C38F600AC9301 /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0857AA201BECDA640044B505 /* VideoDownload.swift */; }; 2C97326C1F4C38F600AC9301 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080DCF161C4518BC00DE3E2E /* SearchResult.swift */; }; 2C97326D1F4C38F600AC9301 /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AEC3A41CCA69F300FFF29E /* Device.swift */; }; 2C97326E1F4C38F600AC9301 /* CyrillicURLActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084070861D64DC7500308FC1 /* CyrillicURLActivityItemSource.swift */; }; - 2C97326F1F4C38F600AC9301 /* StepsControllerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082EDED21D881562006B51DC /* StepsControllerRouter.swift */; }; 2C9732701F4C38F600AC9301 /* StepsControllerDeepLinkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF51D8C835600B27C95 /* StepsControllerDeepLinkRouter.swift */; }; 2C9732711F4C38F600AC9301 /* UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB6A1DC8EB45006622EC /* UserActivity.swift */; }; 2C9732721F4C38F600AC9301 /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083749251DE5AE0400144C14 /* Alerts.swift */; }; @@ -3853,14 +3855,13 @@ 864D67111E83DE03001E8D9E /* StepicsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AB82341D74926F00FDEADE /* StepicsAPI.swift */; }; 864D67121E83DE03001E8D9E /* UnitsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF21D89C86F00B27C95 /* UnitsAPI.swift */; }; 864D67131E83DE03001E8D9E /* UserActivitiesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB671DC8DF59006622EC /* UserActivitiesAPI.swift */; }; - 864D67151E83DE03001E8D9E /* JSONInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */; }; + 864D67151E83DE03001E8D9E /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */; }; 864D67161E83DE03001E8D9E /* TabsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74601BD8159F0064AAEA /* TabsInfo.swift */; }; 864D67171E83DE03001E8D9E /* VideosInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74631BD9213D0064AAEA /* VideosInfo.swift */; }; 864D67181E83DE03001E8D9E /* VideoDownload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0857AA201BECDA640044B505 /* VideoDownload.swift */; }; 864D67191E83DE03001E8D9E /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080DCF161C4518BC00DE3E2E /* SearchResult.swift */; }; 864D671A1E83DE03001E8D9E /* Device.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08AEC3A41CCA69F300FFF29E /* Device.swift */; }; 864D671B1E83DE03001E8D9E /* CyrillicURLActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084070861D64DC7500308FC1 /* CyrillicURLActivityItemSource.swift */; }; - 864D671C1E83DE03001E8D9E /* StepsControllerRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082EDED21D881562006B51DC /* StepsControllerRouter.swift */; }; 864D671D1E83DE03001E8D9E /* StepsControllerDeepLinkRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 085C4FF51D8C835600B27C95 /* StepsControllerDeepLinkRouter.swift */; }; 864D671E1E83DE03001E8D9E /* UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08E6BB6A1DC8EB45006622EC /* UserActivity.swift */; }; 864D671F1E83DE03001E8D9E /* Alerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083749251DE5AE0400144C14 /* Alerts.swift */; }; @@ -4065,7 +4066,7 @@ 938860D42030E60500DFEA3A /* ImageConvertableCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938860D32030E60500DFEA3A /* ImageConvertableCollectionViewCell.swift */; }; 938860D92036571B00DFEA3A /* RightDetailedCustomTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938860D82036571B00DFEA3A /* RightDetailedCustomTableViewCell.swift */; }; 938860DB2037471900DFEA3A /* VideoStepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938860DA2037471900DFEA3A /* VideoStepViewController.swift */; }; - 938860E1203799BA00DFEA3A /* JSONInitializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */; }; + 938860E1203799BA00DFEA3A /* JSONSerializable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */; }; 938860E2203799D900DFEA3A /* CoursesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080CE1421E955D300089A27F /* CoursesAPI.swift */; }; 938860E320379A4B00DFEA3A /* Course.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D1EF721BB5636700BE84E6 /* Course.swift */; }; 938860E420379A6300DFEA3A /* Course+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D1EF711BB5636700BE84E6 /* Course+CoreDataProperties.swift */; }; @@ -4465,7 +4466,7 @@ 0828FF7B1BC7F9D6000AFEA7 /* Unit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Unit.swift; sourceTree = ""; }; 0828FF7E1BC7FD24000AFEA7 /* Lesson+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Lesson+CoreDataProperties.swift"; sourceTree = ""; }; 0828FF7F1BC7FD24000AFEA7 /* Lesson.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lesson.swift; sourceTree = ""; }; - 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONInitializable.swift; sourceTree = ""; }; + 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONSerializable.swift; sourceTree = ""; }; 0828FF851BC81EEC000AFEA7 /* UnitsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitsViewController.swift; sourceTree = ""; }; 0828FF8A1BC81F41000AFEA7 /* UnitTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnitTableViewCell.swift; sourceTree = ""; }; 0828FF8B1BC81F41000AFEA7 /* UnitTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = UnitTableViewCell.xib; sourceTree = ""; }; @@ -4480,7 +4481,6 @@ 082CB1B41D08971900C79A27 /* DiscussionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscussionsViewController.swift; sourceTree = ""; }; 082CB1B51D08971900C79A27 /* DiscussionsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DiscussionsViewController.xib; sourceTree = ""; }; 082E5E0D1F46379100F41426 /* ReplyCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyCache.swift; sourceTree = ""; }; - 082EDED21D881562006B51DC /* StepsControllerRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepsControllerRouter.swift; sourceTree = ""; }; 082EDED51D88871F006B51DC /* Model_v9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model_v9.xcdatamodel; sourceTree = ""; }; 082FD64B1D6C849C007F3E07 /* ReplaceLastSegue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplaceLastSegue.swift; sourceTree = ""; }; 083267A51CDCE64F002F7B5A /* PersistentTaskRecoveryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistentTaskRecoveryManager.swift; sourceTree = ""; }; @@ -4510,7 +4510,7 @@ 083D649B1C172015003222F0 /* StepicApplicationsInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StepicApplicationsInfo.swift; sourceTree = ""; }; 083D64AE1C19BDB2003222F0 /* ControllerHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerHelper.swift; sourceTree = ""; }; 083E1DA51C96E9F100B305E4 /* ApplicationInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationInfo.swift; sourceTree = ""; }; - 083F2B0D1E9D87C000714173 /* CertificatesAPITest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificatesAPITest.swift; sourceTree = ""; }; + 083E49DC2072B684004896C0 /* IDFetchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IDFetchable.swift; sourceTree = ""; }; 083F2B101E9D8E8F00714173 /* CertificatesStoryboard.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = CertificatesStoryboard.storyboard; sourceTree = ""; }; 083F2B131E9D8EF800714173 /* CertificatesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificatesViewController.swift; sourceTree = ""; }; 083F2B161E9D8F1D00714173 /* CertificatesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CertificatesView.swift; sourceTree = ""; }; @@ -4582,6 +4582,7 @@ 0861E6711CD80A9600B45652 /* Executable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Executable.swift; sourceTree = ""; }; 0861E6741CD8106E00B45652 /* ExecutionQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExecutionQueue.swift; sourceTree = ""; }; 0861E67A1CD9483500B45652 /* ExecutionQueues.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExecutionQueues.swift; sourceTree = ""; }; + 0863B9662069A41A0023A182 /* RetrieveRequestMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetrieveRequestMaker.swift; sourceTree = ""; }; 086494841E8C753B0083E0BE /* SberbankUniversity-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "SberbankUniversity-Info.plist"; path = "Sb/SberbankUniversity-Info.plist"; sourceTree = ""; }; 086494851E8C753B0083E0BE /* SberbankUniversity.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = SberbankUniversity.xcassets; path = Sb/SberbankUniversity.xcassets; sourceTree = ""; }; 086494861E8C753B0083E0BE /* SberbankUniversity.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = SberbankUniversity.entitlements; path = Sb/SberbankUniversity.entitlements; sourceTree = ""; }; @@ -4655,6 +4656,11 @@ 088CB1EB1D4BD5ED00C6ED1B /* FreeAnswerDataset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FreeAnswerDataset.swift; sourceTree = ""; }; 088E58C01DE34E2F0009B9CE /* SocialSDKProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocialSDKProvider.swift; sourceTree = ""; }; 088E58C31DE34ED20009B9CE /* VKSocialSDKProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VKSocialSDKProvider.swift; sourceTree = ""; }; + 088E73E92060124B00D458E3 /* ApiRequestRetrier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiRequestRetrier.swift; sourceTree = ""; }; + 088E73EC20614DFC00D458E3 /* UpdateRequestMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateRequestMaker.swift; sourceTree = ""; }; + 088E73EF20619C8F00D458E3 /* DeleteRequestMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteRequestMaker.swift; sourceTree = ""; }; + 088E73F120619EEF00D458E3 /* CreateRequestMaker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateRequestMaker.swift; sourceTree = ""; }; + 088E73F32061BDAA00D458E3 /* StepikModelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepikModelView.swift; sourceTree = ""; }; 088EDB4F1F8E8A5D009B736E /* CourseListVerticalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseListVerticalViewController.swift; sourceTree = ""; }; 088EDB581F8F5A48009B736E /* CourseListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseListViewController.swift; sourceTree = ""; }; 088EDCA51F9496DE0098DEC7 /* CourseListHorizontalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseListHorizontalViewController.swift; sourceTree = ""; }; @@ -4803,6 +4809,7 @@ 08D5F5761F7DA8CB007C1634 /* CourseReviewSummary+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CourseReviewSummary+CoreDataProperties.swift"; sourceTree = ""; }; 08D5F57F1F7DBA70007C1634 /* CourseReviewSummariesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseReviewSummariesAPI.swift; sourceTree = ""; }; 08D674921D78B46900B60963 /* AuthNavigationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthNavigationViewController.swift; sourceTree = ""; }; + 08D9E98B206C243D002F41D3 /* DatabaseFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFetchService.swift; sourceTree = ""; }; 08DA79001DB6BB36003491C4 /* ControllerHelperLaunchExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ControllerHelperLaunchExtension.swift; sourceTree = ""; }; 08DB7DAA1D50F92B0006E9F6 /* Stepic.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Stepic.entitlements; sourceTree = ""; }; 08DB8CB71D0BECF000A6D079 /* DiscussionTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiscussionTableViewCell.swift; sourceTree = ""; }; @@ -5602,7 +5609,6 @@ 081058321E9FB50700FAC30A /* Certificates */ = { isa = PBXGroup; children = ( - 083F2B0D1E9D87C000714173 /* CertificatesAPITest.swift */, 080AA22F1EA017FB0079272F /* CertificateSpec.swift */, ); name = Certificates; @@ -6031,6 +6037,15 @@ 084F7AAF1E775A410088368A /* API Endpoints */ = { isa = PBXGroup; children = ( + 2C9E3F3D1F7A930100DDF1AA /* NotificationsAPI.swift */, + 086A8B271D21796800F45C45 /* VotesAPI.swift */, + 08AB82341D74926F00FDEADE /* StepicsAPI.swift */, + 085C4FF21D89C86F00B27C95 /* UnitsAPI.swift */, + 08E6BB671DC8DF59006622EC /* UserActivitiesAPI.swift */, + 86795EF61E85325000A985C2 /* RecommendationsAPI.swift */, + 08964BCC1F3072BA00DBBCCE /* QueriesAPI.swift */, + 2C8CB0E21FB48F39008CB1AC /* EnrollmentsAPI.swift */, + 2CA9D9842010EEA2007AA743 /* AdaptiveRatingsAPI.swift */, 08AEC3A21CCA69C700FFF29E /* DevicesAPI.swift */, 0800B81E1D06FDE4006C987E /* DiscussionProxiesAPI.swift */, 0800B8211D07029B006C987E /* CommentsAPI.swift */, @@ -6286,7 +6301,7 @@ 0885F8551BA9F18900F2A188 /* Parser.swift */, 080F74621BD921130064AAEA /* Stepic */, 0885F84B1BA8376D00F2A188 /* Network */, - 0828FF821BC800C0000AFEA7 /* JSONInitializable.swift */, + 0828FF821BC800C0000AFEA7 /* JSONSerializable.swift */, 080F74601BD8159F0064AAEA /* TabsInfo.swift */, 080F74631BD9213D0064AAEA /* VideosInfo.swift */, 0857AA201BECDA640044B505 /* VideoDownload.swift */, @@ -6294,12 +6309,14 @@ 080DCF161C4518BC00DE3E2E /* SearchResult.swift */, 08AEC3A41CCA69F300FFF29E /* Device.swift */, 084070861D64DC7500308FC1 /* CyrillicURLActivityItemSource.swift */, - 082EDED21D881562006B51DC /* StepsControllerRouter.swift */, 085C4FF51D8C835600B27C95 /* StepsControllerDeepLinkRouter.swift */, 08E6BB6A1DC8EB45006622EC /* UserActivity.swift */, 083749281DE5AE1400144C14 /* Alerts */, 083749361DE5C7DC00144C14 /* LocalNotificationManager.swift */, 0884696B1E7C2E00009131E9 /* LastStepGlobalContext.swift */, + 088E73F32061BDAA00D458E3 /* StepikModelView.swift */, + 08D9E98B206C243D002F41D3 /* DatabaseFetchService.swift */, + 083E49DC2072B684004896C0 /* IDFetchable.swift */, ); name = Model; sourceTree = ""; @@ -6346,16 +6363,8 @@ 0885F8531BA9DB5C00F2A188 /* Meta.swift */, 084F7AAF1E775A410088368A /* API Endpoints */, 08AEC3A61CCA75F400FFF29E /* APIDefaults.swift */, - 2C9E3F3D1F7A930100DDF1AA /* NotificationsAPI.swift */, 0800B8241D0704B6006C987E /* ApiUtil.swift */, - 086A8B271D21796800F45C45 /* VotesAPI.swift */, - 08AB82341D74926F00FDEADE /* StepicsAPI.swift */, - 085C4FF21D89C86F00B27C95 /* UnitsAPI.swift */, - 08E6BB671DC8DF59006622EC /* UserActivitiesAPI.swift */, - 86795EF61E85325000A985C2 /* RecommendationsAPI.swift */, - 08964BCC1F3072BA00DBBCCE /* QueriesAPI.swift */, - 2C8CB0E21FB48F39008CB1AC /* EnrollmentsAPI.swift */, - 2CA9D9842010EEA2007AA743 /* AdaptiveRatingsAPI.swift */, + 088E73EE20619C7800D458E3 /* Request Makers */, ); name = Network; sourceTree = ""; @@ -6422,6 +6431,17 @@ name = SocialSDKProviders; sourceTree = ""; }; + 088E73EE20619C7800D458E3 /* Request Makers */ = { + isa = PBXGroup; + children = ( + 088E73EC20614DFC00D458E3 /* UpdateRequestMaker.swift */, + 088E73EF20619C8F00D458E3 /* DeleteRequestMaker.swift */, + 088E73F120619EEF00D458E3 /* CreateRequestMaker.swift */, + 0863B9662069A41A0023A182 /* RetrieveRequestMaker.swift */, + ); + name = "Request Makers"; + sourceTree = ""; + }; 088EDCAE1F949A2C0098DEC7 /* New Widgets Collection View Cell */ = { isa = PBXGroup; children = ( @@ -6839,6 +6859,7 @@ 08DE94131B8C58AC00D278AB /* Stepic */ = { isa = PBXGroup; children = ( + 088E73E92060124B00D458E3 /* ApiRequestRetrier.swift */, 084C658B1FDAAD35006A3E17 /* Remote Config */, 08DF78D01F64056B00AEEA85 /* UI Elements */, 08DB7DAA1D50F92B0006E9F6 /* Stepic.entitlements */, @@ -11467,7 +11488,6 @@ buildActionMask = 2147483647; files = ( 2CB51F6B204FC6220008431C /* UserActivitySpec.swift in Sources */, - 083F2B0E1E9D87C000714173 /* CertificatesAPITest.swift in Sources */, 08AB823B1D75AA9300FDEADE /* CookieTests.swift in Sources */, 080AA2301EA017FB0079272F /* CertificateSpec.swift in Sources */, 085D5CE41D007F6500092060 /* HTMLParsingTests.swift in Sources */, @@ -11592,7 +11612,6 @@ 08EDD6191F7C607A005203E4 /* CourseWidgetTableViewCell.swift in Sources */, 0869F6D31CE216B600F8A6DB /* PersistentQueueRecoveryManager.swift in Sources */, 08BC47071CD9F424009A1D25 /* DeleteDeviceExecutableTask.swift in Sources */, - 082EDED41D881562006B51DC /* StepsControllerRouter.swift in Sources */, 08DF78BA1F5E0C9300AEEA85 /* StringHelper.swift in Sources */, 08A500511F2B73C000140D25 /* FullHeightWebView.swift in Sources */, 08AF59F71E6D9BE800423EFF /* RGPageViewController+UIToolbarDelegate.swift in Sources */, @@ -11736,6 +11755,7 @@ 0829B83D1E9D05AE009B4A84 /* Certificate+CoreDataProperties.swift in Sources */, 088EDCB21F949B0F0098DEC7 /* CourseWidgetCollectionViewCell.swift in Sources */, 080CE15F1E9580CD0089A27F /* AttemptsAPI.swift in Sources */, + 088E73EB206014B000D458E3 /* AdaptiveRatingsAPI.swift in Sources */, 086A8B261D21434B00F45C45 /* Vote.swift in Sources */, 08D120D41C937B2200A54ABC /* GeneralInfoTableViewCell.swift in Sources */, 08D120D51C937B2200A54ABC /* NumberQuizViewController.swift in Sources */, @@ -11790,7 +11810,7 @@ 083749311DE5BBF700144C14 /* PreferencesContainer.swift in Sources */, 08D120EA1C937B2200A54ABC /* TextReply.swift in Sources */, 082FD64D1D6C849C007F3E07 /* ReplaceLastSegue.swift in Sources */, - 08D120EB1C937B2200A54ABC /* JSONInitializable.swift in Sources */, + 08D120EB1C937B2200A54ABC /* JSONSerializable.swift in Sources */, 08D120EC1C937B2200A54ABC /* UIImageViewExtension.swift in Sources */, 0864949E1E8C79530083E0BE /* SbAppDelegate.swift in Sources */, 08D120ED1C937B2200A54ABC /* Lesson+CoreDataProperties.swift in Sources */, @@ -11963,6 +11983,7 @@ 0860D9151F10EA690087D61B /* InputAccessoryBuilder.swift in Sources */, 08D2AE4A1C05127500BD8C3D /* AnalyticsHelper.swift in Sources */, 0860D9121F10C5480087D61B /* CodeSnippetSymbols.swift in Sources */, + 083E49DD2072B684004896C0 /* IDFetchable.swift in Sources */, 08CBA3271F57563C00302154 /* TransitionMenuBlockTableViewCell.swift in Sources */, 08F261211E79DD0C00AC908B /* APIEndpoint.swift in Sources */, 0828FF811BC7FD24000AFEA7 /* Lesson.swift in Sources */, @@ -12040,6 +12061,7 @@ 084F7AA71E76EF690088368A /* LastStep.swift in Sources */, 0829B83C1E9D05AE009B4A84 /* Certificate+CoreDataProperties.swift in Sources */, 083540611CE5DD5000BDFEA5 /* NotificationReactionHandler.swift in Sources */, + 088E73ED20614DFC00D458E3 /* UpdateRequestMaker.swift in Sources */, 08CA59EE1BBFC962008DC44D /* ButtonExtension.swift in Sources */, 08DB8CB91D0BECF000A6D079 /* DiscussionTableViewCell.swift in Sources */, 08B9770C1D19D5AA00FFC52C /* DiscussionWebTableViewCell.swift in Sources */, @@ -12102,7 +12124,6 @@ 2CE3BCA71FBF13CE000AD405 /* SQLReply.swift in Sources */, 0885F8561BA9F18900F2A188 /* Parser.swift in Sources */, 087585A31FB50D640047A269 /* CourseListsAPI.swift in Sources */, - 082EDED31D881562006B51DC /* StepsControllerRouter.swift in Sources */, 08A9F70D1FC3837800640F1F /* CourseTagCollectionViewCell.swift in Sources */, 0889FEB21BFB864B00C6417E /* DownloadTableViewCell.swift in Sources */, 089B370F1BFA210F003DC593 /* CacheManager.swift in Sources */, @@ -12130,6 +12151,7 @@ 0869530F1D747DC0003857A2 /* RequestChain.swift in Sources */, 082A88FE2046F0460079F038 /* NotificationPermissionManager.swift in Sources */, 081959E01FA0032E00E6D6CD /* HomeScreenPresenter.swift in Sources */, + 08D9E98C206C243D002F41D3 /* DatabaseFetchService.swift in Sources */, 082BE3AE1E676373006BC60F /* AuthRoutingManager.swift in Sources */, 089056111E98021000B8FE6A /* RateAppViewController.swift in Sources */, 08F5555A1C4FB0A300C877E8 /* Reply.swift in Sources */, @@ -12139,11 +12161,13 @@ 2CC3519C1F6837B4004255B6 /* RegistrationViewController.swift in Sources */, 08C5E4FD1C315272004AA626 /* AudioManager.swift in Sources */, 2C7FEE7F1FDFCEF200B2B4F1 /* OnboardingAnimatedView.swift in Sources */, + 088E73F42061BDAA00D458E3 /* StepikModelView.swift in Sources */, 2CC3519A1F68339A004255B6 /* AuthTextField.swift in Sources */, 08CBA3151F57562A00302154 /* SwitchMenuBlockTableViewCell.swift in Sources */, 085DF8D31C99A8F9006809D9 /* PlayerTestViewController.swift in Sources */, 2C5DF13B1FEC0534003B1177 /* CardsStepsViewController.swift in Sources */, 083267A91CDCF59B002F7B5A /* DictionarySerializable.swift in Sources */, + 0863B9672069A41A0023A182 /* RetrieveRequestMaker.swift in Sources */, 080F74641BD9213D0064AAEA /* VideosInfo.swift in Sources */, 08E8F9671F34DD2C008CF4A1 /* SearchQueriesPresenter.swift in Sources */, 08D1EF731BB5636700BE84E6 /* Course+CoreDataProperties.swift in Sources */, @@ -12152,6 +12176,7 @@ 0899842F1ECDE19E005C0B27 /* LessonView.swift in Sources */, 0891424B1BCEE4EF0000BCB0 /* VideoURL.swift in Sources */, 080EBA371EA64C0C00C43C93 /* CertificatesPresentationContainer.swift in Sources */, + 088E73EA2060124B00D458E3 /* ApiRequestRetrier.swift in Sources */, 0885B8D21FB64452005A7B2E /* ContentLanguageCollectionViewCell.swift in Sources */, 2CF08864205BEF3C00FCB9C0 /* StepikPlaceholderView.swift in Sources */, 2C6E9CDB1FF27543001821A2 /* AdaptiveStorageManager.swift in Sources */, @@ -12246,7 +12271,7 @@ 2CF0885A205BEBF500FCB9C0 /* StepikTableView.swift in Sources */, 089B370D1BFA07AF003DC593 /* VideoDownloadDelegate.swift in Sources */, 08F4859A1C57868E000165AA /* TextReply.swift in Sources */, - 0828FF831BC800C0000AFEA7 /* JSONInitializable.swift in Sources */, + 0828FF831BC800C0000AFEA7 /* JSONSerializable.swift in Sources */, 08CA59F41BC020E3008DC44D /* UIImageViewExtension.swift in Sources */, 086D5B3F20127A25000F7715 /* Tooltip.swift in Sources */, 0837492A1DE5AF8A00144C14 /* StreaksAlertManager.swift in Sources */, @@ -12309,6 +12334,7 @@ 08CBA3671F5B2B8800302154 /* ProfileStreaksView.swift in Sources */, 080C9A2C1BAC88E2001BE326 /* UICustomizer.swift in Sources */, 2CD8464B1F25FE8B00E8153C /* ProfilesAPI.swift in Sources */, + 088E73F020619C8F00D458E3 /* DeleteRequestMaker.swift in Sources */, 0885F8521BA9D64400F2A188 /* Constants.swift in Sources */, 08BC47061CD9F424009A1D25 /* DeleteDeviceExecutableTask.swift in Sources */, 085514EB1CFB09760080CB88 /* CellWebViewHelper.swift in Sources */, @@ -12344,6 +12370,7 @@ 2C4533A2204DB3D00061342A /* PinsMapExpandableMenuBlockTableViewCell.swift in Sources */, 0841BDC71E082AE7008CE13E /* WatchDataHelper.swift in Sources */, 86624A751FC76682008E7E6C /* NotificationsStatus.swift in Sources */, + 088E73F220619EEF00D458E3 /* CreateRequestMaker.swift in Sources */, 08AC214B1CE0DE9B00FBB9CD /* DeviceDefaults.swift in Sources */, 08964BCD1F3072BA00DBBCCE /* QueriesAPI.swift in Sources */, 084C658D1FDAD04C006A3E17 /* RemoteConfig.swift in Sources */, @@ -12525,7 +12552,7 @@ 93CC6832204980C900F5BE75 /* NotificationsStatus.swift in Sources */, 938860EF20379B5B00DFEA3A /* Dataset.swift in Sources */, 937E1CFB1FDD766800F34045 /* CourseTag.swift in Sources */, - 938860E1203799BA00DFEA3A /* JSONInitializable.swift in Sources */, + 938860E1203799BA00DFEA3A /* JSONSerializable.swift in Sources */, 938860FC20379C6700DFEA3A /* VideoURL.swift in Sources */, 936351C12029116B00DFC7EE /* NotificationNameExtension.swift in Sources */, 938861472037E49700DFEA3A /* ApiUtil.swift in Sources */, @@ -13019,7 +13046,7 @@ 2C608B5520456BD6006870BB /* NotificationDataExtractor.swift in Sources */, 089C88E41FCF494300003B63 /* CourseListType.swift in Sources */, 08CBA3511F57734900302154 /* ProfileViewController.swift in Sources */, - 2C1B62201F4C4AEF00236804 /* JSONInitializable.swift in Sources */, + 2C1B62201F4C4AEF00236804 /* JSONSerializable.swift in Sources */, 2C1B62211F4C4AEF00236804 /* TabsInfo.swift in Sources */, 0844900F1F5D67E300D01940 /* SettingsViewController.swift in Sources */, 2C1B62221F4C4AEF00236804 /* VideosInfo.swift in Sources */, @@ -13030,7 +13057,6 @@ 2C608AF720456926006870BB /* CardStepViewController.swift in Sources */, 2C1B62261F4C4AEF00236804 /* CyrillicURLActivityItemSource.swift in Sources */, 2C608B3B20456A04006870BB /* MigrationExtensions.swift in Sources */, - 2C1B62271F4C4AEF00236804 /* StepsControllerRouter.swift in Sources */, 2C1B62281F4C4AEF00236804 /* StepsControllerDeepLinkRouter.swift in Sources */, 2CF9532A2062A20A00B9617A /* StepikPlaceholderView.swift in Sources */, 2C1B62291F4C4AEF00236804 /* UserActivity.swift in Sources */, @@ -13503,7 +13529,7 @@ 2C608B5320456BD5006870BB /* NotificationDataExtractor.swift in Sources */, 089C88E51FCF494300003B63 /* CourseListType.swift in Sources */, 08CBA3521F57734900302154 /* ProfileViewController.swift in Sources */, - 2C1B64311F4C590700236804 /* JSONInitializable.swift in Sources */, + 2C1B64311F4C590700236804 /* JSONSerializable.swift in Sources */, 2C1B64321F4C590700236804 /* TabsInfo.swift in Sources */, 084490101F5D67E300D01940 /* SettingsViewController.swift in Sources */, 2C1B64331F4C590700236804 /* VideosInfo.swift in Sources */, @@ -13514,7 +13540,6 @@ 2C608AF820456927006870BB /* CardStepViewController.swift in Sources */, 2C1B64371F4C590700236804 /* CyrillicURLActivityItemSource.swift in Sources */, 2C608B3A20456A03006870BB /* MigrationExtensions.swift in Sources */, - 2C1B64381F4C590700236804 /* StepsControllerRouter.swift in Sources */, 2C1B64391F4C590700236804 /* StepsControllerDeepLinkRouter.swift in Sources */, 2CF953292062A20A00B9617A /* StepikPlaceholderView.swift in Sources */, 2C1B643A1F4C590700236804 /* UserActivity.swift in Sources */, @@ -13990,7 +14015,7 @@ 2C89AAB11F4C289900227C3B /* UserActivitiesAPI.swift in Sources */, 08CBA34F1F57734900302154 /* ProfileViewController.swift in Sources */, 08B062E11FDEFD5900A6C999 /* StreaksAlertPresentationManager.swift in Sources */, - 2C89AAB41F4C289900227C3B /* JSONInitializable.swift in Sources */, + 2C89AAB41F4C289900227C3B /* JSONSerializable.swift in Sources */, 2C89AAB51F4C289900227C3B /* TabsInfo.swift in Sources */, 0844900D1F5D67E300D01940 /* SettingsViewController.swift in Sources */, 2C89AAB61F4C289900227C3B /* VideosInfo.swift in Sources */, @@ -13999,7 +14024,6 @@ 2C89AAB81F4C289900227C3B /* SearchResult.swift in Sources */, 2C89AAB91F4C289900227C3B /* Device.swift in Sources */, 2C89AABA1F4C289900227C3B /* CyrillicURLActivityItemSource.swift in Sources */, - 2C89AABB1F4C289900227C3B /* StepsControllerRouter.swift in Sources */, 2C89AABC1F4C289900227C3B /* StepsControllerDeepLinkRouter.swift in Sources */, 2C89AABD1F4C289900227C3B /* UserActivity.swift in Sources */, 2C89AABE1F4C289900227C3B /* Alerts.swift in Sources */, @@ -14470,7 +14494,7 @@ 2C608B5420456BD5006870BB /* NotificationDataExtractor.swift in Sources */, 089C88E31FCF494300003B63 /* CourseListType.swift in Sources */, 08CBA3501F57734900302154 /* ProfileViewController.swift in Sources */, - 2C9732681F4C38F600AC9301 /* JSONInitializable.swift in Sources */, + 2C9732681F4C38F600AC9301 /* JSONSerializable.swift in Sources */, 2C9732691F4C38F600AC9301 /* TabsInfo.swift in Sources */, 0844900E1F5D67E300D01940 /* SettingsViewController.swift in Sources */, 2C97326A1F4C38F600AC9301 /* VideosInfo.swift in Sources */, @@ -14481,7 +14505,6 @@ 2C608AF620456926006870BB /* CardStepViewController.swift in Sources */, 2C97326E1F4C38F600AC9301 /* CyrillicURLActivityItemSource.swift in Sources */, 2C608B3C20456A04006870BB /* MigrationExtensions.swift in Sources */, - 2C97326F1F4C38F600AC9301 /* StepsControllerRouter.swift in Sources */, 2C9732701F4C38F600AC9301 /* StepsControllerDeepLinkRouter.swift in Sources */, 2CF9532B2062A20B00B9617A /* StepikPlaceholderView.swift in Sources */, 2C9732711F4C38F600AC9301 /* UserActivity.swift in Sources */, @@ -15019,7 +15042,7 @@ 864D67131E83DE03001E8D9E /* UserActivitiesAPI.swift in Sources */, 089C88E01FCF494300003B63 /* CourseListType.swift in Sources */, 08CBA34D1F57734900302154 /* ProfileViewController.swift in Sources */, - 864D67151E83DE03001E8D9E /* JSONInitializable.swift in Sources */, + 864D67151E83DE03001E8D9E /* JSONSerializable.swift in Sources */, 864D67161E83DE03001E8D9E /* TabsInfo.swift in Sources */, 0844900B1F5D67E300D01940 /* SettingsViewController.swift in Sources */, 864D67171E83DE03001E8D9E /* VideosInfo.swift in Sources */, @@ -15028,7 +15051,6 @@ 864D67191E83DE03001E8D9E /* SearchResult.swift in Sources */, 864D671A1E83DE03001E8D9E /* Device.swift in Sources */, 864D671B1E83DE03001E8D9E /* CyrillicURLActivityItemSource.swift in Sources */, - 864D671C1E83DE03001E8D9E /* StepsControllerRouter.swift in Sources */, 864D671D1E83DE03001E8D9E /* StepsControllerDeepLinkRouter.swift in Sources */, 864D671E1E83DE03001E8D9E /* UserActivity.swift in Sources */, 864D671F1E83DE03001E8D9E /* Alerts.swift in Sources */, diff --git a/Stepic.xcodeproj/xcuserdata/Ostrenkiy.xcuserdatad/xcschemes/xcschememanagement.plist b/Stepic.xcodeproj/xcuserdata/Ostrenkiy.xcuserdatad/xcschemes/xcschememanagement.plist index 78d30c0ae5..dc5f7e577e 100644 --- a/Stepic.xcodeproj/xcuserdata/Ostrenkiy.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Stepic.xcodeproj/xcuserdata/Ostrenkiy.xcuserdatad/xcschemes/xcschememanagement.plist @@ -118,6 +118,11 @@ orderHint 86 + Adaptive GMAT.xcscheme + + orderHint + 95 + SberbankUniversity.xcscheme isShown @@ -154,7 +159,7 @@ StepikTV.xcscheme_^#shared#^_ orderHint - 87 + 96 StickerPackExtension.xcscheme diff --git a/Stepic/APIEndpoint.swift b/Stepic/APIEndpoint.swift index b7632921f8..80b3f8845f 100644 --- a/Stepic/APIEndpoint.swift +++ b/Stepic/APIEndpoint.swift @@ -36,10 +36,23 @@ class APIEndpoint { let manager: Alamofire.SessionManager + var update: UpdateRequestMaker + var delete: DeleteRequestMaker + var create: CreateRequestMaker + var retrieve: RetrieveRequestMaker + init() { let configuration = URLSessionConfiguration.default configuration.timeoutIntervalForRequest = 15 manager = Alamofire.SessionManager(configuration: configuration) + let retrier = ApiRequestRetrier() + manager.retrier = retrier + manager.adapter = retrier + + update = UpdateRequestMaker() + delete = DeleteRequestMaker() + create = CreateRequestMaker() + retrieve = RetrieveRequestMaker() } func cancelAllTasks() { @@ -60,123 +73,23 @@ class APIEndpoint { return result } - func getObjectsByIds(ids: [T.idType], updating: [T], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, printOutput: Bool = false) -> Promise<([T])> { - let name = self.name - return Promise<([T])> { - fulfill, reject in - let params: Parameters = [ - "ids": ids - ] - - manager.request("\(StepicApplicationsInfo.apiURL)/\(name)", parameters: params, encoding: URLEncoding.default, headers: headers).validate().responseSwiftyJSON { response in - switch response.result { - - case .failure(let error): - reject(RetrieveError(error: error)) - - case .success(let json): - let jsonArray: [JSON] = json[name].array ?? [] - let resultArray: [T] = jsonArray.map { - objectJSON in - if let recoveredIndex = updating.index(where: { $0.hasEqualId(json: objectJSON) }) { - updating[recoveredIndex].update(json: objectJSON) - return updating[recoveredIndex] - } else { - return T(json: objectJSON) - } - } - - CoreDataHelper.instance.save() - fulfill((resultArray)) - } - - } - } + //TODO: Remove this in next refactoring iterations + func getObjectsByIds(ids: [T.IdType], updating: [T], printOutput: Bool = false) -> Promise<([T])> { + return retrieve.request(requestEndpoint: name, paramName: name, ids: ids, updating: updating, withManager: manager) } - func getObjectsByIds(requestString: String, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, printOutput: Bool = false, ids: [T.idType], deleteObjects: [T], refreshMode: RefreshMode, success: (([T]) -> Void)?, failure : @escaping (_ error: RetrieveError) -> Void) -> Request? { - - let params: Parameters = [:] - - let idString = constructIdsString(array: ids) - if idString == "" { - success?([]) - return nil - } - - return manager.request("\(StepicApplicationsInfo.apiURL)/\(requestString)?\(idString)", parameters: params, encoding: URLEncoding.default, headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } - let response = response.response - - if printOutput { - print(json) - } - - if let e = error as NSError? { - print("RETRIEVE \(requestString)?\(ids): error \(e.domain) \(e.code): \(e.localizedDescription)") - if e.code == -999 { - failure(.cancelled) - return - } else { - failure(.connectionError) - return - } - } - - if response?.statusCode != 200 { - print("RETRIEVE \(requestString)?\(ids)): bad response status code \(String(describing: response?.statusCode))") - failure(.badStatus) + func getObjectsByIds(requestString: String, printOutput: Bool = false, ids: [T.IdType], deleteObjects: [T], refreshMode: RefreshMode, success: (([T]) -> Void)?, failure : @escaping (_ error: RetrieveError) -> Void) -> Request? { + getObjectsByIds(ids: ids, updating: deleteObjects).then { + objects in + success?(objects) + }.catch { + error in + guard let e = error as? RetrieveError else { + failure(RetrieveError(error: error)) return } - - var newObjects: [T] = [] - - switch refreshMode { - - case .delete: - - for object in deleteObjects { - CoreDataHelper.instance.deleteFromStore(object as! NSManagedObject, save: false) - } - - for objectJSON in json[requestString].arrayValue { - newObjects += [T(json: objectJSON)] - } - - case .update: - - for objectJSON in json[requestString].arrayValue { - let existing = deleteObjects.filter({obj in obj.hasEqualId(json: objectJSON)}) - - switch existing.count { - case 0: - newObjects += [T(json: objectJSON)] - case 1: - let obj = existing[0] - obj.update(json: objectJSON) - newObjects += [obj] - default: - //TODO: Fix this in the next releases! We have some problems with deleting entities from CoreData - let obj = existing[0] - obj.update(json: objectJSON) - newObjects += [obj] - print("More than 1 object with the same id!") - } - } - } - - CoreDataHelper.instance.save() - success?(newObjects) - }) + failure(e) + } + return nil } } diff --git a/Stepic/AdaptiveRatingsAPI.swift b/Stepic/AdaptiveRatingsAPI.swift index f332b14470..0f0e8b011c 100644 --- a/Stepic/AdaptiveRatingsAPI.swift +++ b/Stepic/AdaptiveRatingsAPI.swift @@ -11,6 +11,7 @@ import Alamofire import SwiftyJSON import PromiseKit +//TODO: Better refactor this to two classes class AdaptiveRatingsAPI: APIEndpoint { override var name: String { return "rating" } var restoreName: String { return "rating-restore" } diff --git a/Stepic/ApiRequestRetrier.swift b/Stepic/ApiRequestRetrier.swift new file mode 100644 index 0000000000..9cc92ed2dd --- /dev/null +++ b/Stepic/ApiRequestRetrier.swift @@ -0,0 +1,34 @@ +// +// ApiRequestRetrier.swift +// Stepic +// +// Created by Ostrenkiy on 18.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// +import Foundation +import Alamofire +import PromiseKit + +class ApiRequestRetrier: RequestRetrier, RequestAdapter { + + func adapt(_ urlRequest: URLRequest) throws -> URLRequest { + var urlRequest = urlRequest + for (headerField, value) in AuthInfo.shared.initialHTTPHeaders { + urlRequest.setValue(value, forHTTPHeaderField: headerField) + } + return urlRequest + } + + func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { + if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 && request.retryCount == 0 { + checkToken().then { + completion(true, 0.0) + }.catch { + _ in + completion(false, 0.0) + } + } else { + completion(false, 0.0) + } + } +} diff --git a/Stepic/AppDelegate.swift b/Stepic/AppDelegate.swift index ae66b767d1..f1c208216e 100644 --- a/Stepic/AppDelegate.swift +++ b/Stepic/AppDelegate.swift @@ -97,9 +97,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return } - checkToken().then { - ApiDataDownloader.notificationsStatusAPI.retrieve() - }.then { result -> Void in + ApiDataDownloader.notificationsStatusAPI.retrieve().then { result -> Void in NotificationsBadgesManager.shared.set(number: result.totalCount) }.catch { _ in print("notifications: unable to fetch badges count on launch") diff --git a/Stepic/Assignment.swift b/Stepic/Assignment.swift index f08b5cee9a..926ba9c1dc 100644 --- a/Stepic/Assignment.swift +++ b/Stepic/Assignment.swift @@ -10,9 +10,10 @@ import Foundation import CoreData import SwiftyJSON -class Assignment: NSManagedObject, JSONInitializable { +@objc +class Assignment: NSManagedObject, JSONSerializable { - typealias idType = Int + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -28,8 +29,4 @@ class Assignment: NSManagedObject, JSONInitializable { func update(json: JSON) { initialize(json) } - - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } } diff --git a/Stepic/AssignmentsAPI.swift b/Stepic/AssignmentsAPI.swift index add88e0a93..e0444f0129 100644 --- a/Stepic/AssignmentsAPI.swift +++ b/Stepic/AssignmentsAPI.swift @@ -14,6 +14,6 @@ class AssignmentsAPI: APIEndpoint { override var name: String { return "assignments" } @discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [Assignment], refreshMode: RefreshMode, success: @escaping (([Assignment]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { - return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) + return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) } } diff --git a/Stepic/Attempt.swift b/Stepic/Attempt.swift index 1f31de424e..ecdeddde53 100644 --- a/Stepic/Attempt.swift +++ b/Stepic/Attempt.swift @@ -9,17 +9,45 @@ import UIKit import SwiftyJSON -class Attempt: NSObject { +class Attempt: JSONSerializable { - var id: Int? + typealias IdType = Int + + var id: Int = 0 var dataset: Dataset? var datasetUrl: String? var time: String? var status: String? - var step: Int + var step: Int = 0 var timeLeft: String? var user: Int? + func update(json: JSON) { + id = json["id"].intValue + datasetUrl = json["dataset_url"].string + time = json["time"].string + status = json["status"].string + step = json["step"].intValue + timeLeft = json["time_left"].string + user = json["user"].int + } + + func hasEqualId(json: JSON) -> Bool { + return id == json["id"].int + } + + init(step: Int) { + self.step = step + } + + required init(json: JSON) { + self.update(json: json) + } + + func initDataset(json: JSON, stepName: String) { + dataset = getDatasetFromJSON(json, stepName: stepName) + } + init(json: JSON, stepName: String) { id = json["id"].intValue dataset = nil @@ -29,10 +57,15 @@ class Attempt: NSObject { step = json["step"].intValue timeLeft = json["time_left"].string user = json["user"].int - super.init() dataset = getDatasetFromJSON(json["dataset"], stepName: stepName) } + var json: JSON { + return [ + "step": step + ] + } + fileprivate func getDatasetFromJSON(_ json: JSON, stepName: String) -> Dataset? { switch stepName { case "choice" : diff --git a/Stepic/AttemptsAPI.swift b/Stepic/AttemptsAPI.swift index 44b1e6e7fe..081b889907 100644 --- a/Stepic/AttemptsAPI.swift +++ b/Stepic/AttemptsAPI.swift @@ -9,52 +9,27 @@ import Foundation import Alamofire import SwiftyJSON +import PromiseKit class AttemptsAPI: APIEndpoint { override var name: String { return "attempts" } - @discardableResult func create(stepName: String, stepId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping (Attempt) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { - - let params: Parameters = [ - "attempt": [ - "step": "\(stepId)" - ] - ] - - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/attempts", method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() + func create(stepName: String, stepId: Int) -> Promise { + let attempt = Attempt(step: stepId) + return Promise { fulfill, reject in + create.request(requestEndpoint: "attempts", paramName: "attempt", creatingObject: attempt, withManager: manager).then { + attempt, json -> Void in + guard let json = json else { + fulfill(attempt) + return } - } else { - json = response.result.value! + attempt.initDataset(json: json["attempts"].arrayValue[0]["dataset"], stepName: stepName) + fulfill(attempt) + }.catch { + error in + reject(error) } - let request = response.request - let response = response.response - - if let e = error { - let d = (e as NSError).localizedDescription - print(d) - errorHandler(d) - return - } - - print("request headers: \(String(describing: request?.allHTTPHeaderFields))") - - if response?.statusCode == 201 { - let attempt = Attempt(json: json["attempts"].arrayValue[0], stepName: stepName) - success(attempt) - return - } else { - errorHandler("Response status code is wrong(\(String(describing: response?.statusCode)))") - return - } - - }) + } } @discardableResult func retrieve(stepName: String, stepId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ([Attempt], Meta) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { @@ -103,3 +78,17 @@ class AttemptsAPI: APIEndpoint { }) } } + +extension AttemptsAPI { + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func create(stepName: String, stepId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping (Attempt) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { + create(stepName: stepName, stepId: stepId).then { + attempt in + success(attempt) + }.catch { + error in + errorHandler(error.localizedDescription) + } + return nil + } +} diff --git a/Stepic/AuthAPI.swift b/Stepic/AuthAPI.swift index 233da8cb2a..f970e422f0 100644 --- a/Stepic/AuthAPI.swift +++ b/Stepic/AuthAPI.swift @@ -114,7 +114,7 @@ class AuthAPI { } case .success(let json): if let r = response.response, - !(200...299 ~= r.statusCode) { + !(200...299 ~= r.statusCode) { switch r.statusCode { case 497: reject(SignInError.manyAttempts) diff --git a/Stepic/AuthInfo.swift b/Stepic/AuthInfo.swift index 98a3774376..f446061a28 100644 --- a/Stepic/AuthInfo.swift +++ b/Stepic/AuthInfo.swift @@ -60,8 +60,8 @@ class AuthInfo: NSObject { course.enrolled = false } + Certificate.deleteAll() Progress.deleteAllStoredProgresses() - Notification.deleteAll() #if !os(tvOS) NotificationsBadgesManager.shared.set(number: 0) diff --git a/Stepic/Certificate.swift b/Stepic/Certificate.swift index e2a1048c75..a381ae5b2c 100644 --- a/Stepic/Certificate.swift +++ b/Stepic/Certificate.swift @@ -10,8 +10,9 @@ import Foundation import CoreData import SwiftyJSON -class Certificate: NSManagedObject, JSONInitializable { - typealias idType = Int +@objc +final class Certificate: NSManagedObject, IDFetchable { + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -34,10 +35,6 @@ class Certificate: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].int - } - class func fetch(_ ids: [Int], user userId: Int) -> [Certificate] { let request = NSFetchRequest(entityName: "Certificate") @@ -56,4 +53,17 @@ class Certificate: NSManagedObject, JSONInitializable { return [] } } + + //TODO: Refactor this action to protocol extension when refactoring CoreData + static func deleteAll() { + let request = NSFetchRequest(entityName: "Certificate") + do { + let results = try CoreDataHelper.instance.context.fetch(request) as? [Certificate] + for obj in results ?? [] { + CoreDataHelper.instance.deleteFromStore(obj) + } + } catch { + print("certificate: couldn't delete all certificates!") + } + } } diff --git a/Stepic/CertificatesAPI.swift b/Stepic/CertificatesAPI.swift index a55cce22b1..290d804c0c 100644 --- a/Stepic/CertificatesAPI.swift +++ b/Stepic/CertificatesAPI.swift @@ -9,74 +9,30 @@ import Foundation import Alamofire import SwiftyJSON +import PromiseKit class CertificatesAPI: APIEndpoint { override var name: String { return "certificates" } - @discardableResult func retrieve(userId: Int, page: Int = 1, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping (Meta, [Certificate]) -> Void, error errorHandler: @escaping (RetrieveError) -> Void) -> Request? { - + func retrieve(userId: Int, page: Int = 1) -> Promise<([Certificate], Meta)> { let params: Parameters = [ "user": userId, "page": page ] - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/\(name)", parameters: params, headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } - let response = response.response - - if let e = error as NSError? { - print("RETRIEVE certificates/\(userId): error \(e.domain) \(e.code): \(e.localizedDescription)") - errorHandler(.connectionError) - return - } - - if response?.statusCode != 200 { - print("RETRIEVE certificates/\(userId): bad response status code \(String(describing: response?.statusCode))") - errorHandler(.badStatus) - return - } - - let meta = Meta(json: json["meta"]) - - //Collect all retrieved ids - - let ids = json["certificates"].arrayValue.flatMap { - $0["id"].int - } - - //Fetch certificates data for all retrieved ids - - let existingCertificates = Certificate.fetch(ids, user: userId) - - //Update existing certificates & create new - - let res: [Certificate] = json["certificates"].arrayValue.map { - certificateJSON in - if let filtered = existingCertificates.filter({$0.hasEqualId(json: certificateJSON)}).first { - filtered.update(json: certificateJSON) - return filtered - } else { - return Certificate(json: certificateJSON) - } - } - - //Return certificates - - success(meta, res) - - return - } - ) + return retrieve.requestWithFetching(requestEndpoint: "certificates", paramName: "certificates", params: params, withManager: manager) } + //Cannot move it to extension cause it is used in tests + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func retrieve(userId: Int, page: Int = 1, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping (Meta, [Certificate]) -> Void, error errorHandler: @escaping (RetrieveError) -> Void) -> Request? { + retrieve(userId: userId, page: page).then { + certificates, meta in + success(meta, certificates) + }.catch { + error in + errorHandler(RetrieveError(error: error)) + } + return nil + } } diff --git a/Stepic/Comment.swift b/Stepic/Comment.swift index 21ab243cab..4bd659bc73 100644 --- a/Stepic/Comment.swift +++ b/Stepic/Comment.swift @@ -16,25 +16,27 @@ enum UserRole: String { /* Comment model, without voting */ -class Comment: JSONInitializable { +class Comment: JSONSerializable { - typealias idType = Int + typealias IdType = Int - var id: Int + var id: Int = 0 var parentId: Int? - var userId: Int - var userRole: UserRole - var time: Date - var lastTime: Date - var text: String - var replyCount: Int - var isDeleted: Bool - var targetStepId: Int - var repliesIds: [Int] - var isPinned: Bool - var voteId: String - var epicCount: Int - var abuseCount: Int + var userId: Int = 0 + var userRole: UserRole = .Student + var time: Date = Date() + var lastTime: Date = Date() + var text: String = "" + var replyCount: Int = 0 + var isDeleted: Bool = false + var targetStepId: Int = 0 + var repliesIds: [Int] = [] + var isPinned: Bool = false + var voteId: String = "" + var epicCount: Int = 0 + var abuseCount: Int = 0 + + //TODO: Check those "!" marks, they look suspicious var userInfo: UserInfo! var vote: Vote! @@ -82,52 +84,19 @@ class Comment: JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } - -// -// init(sampleId: Int) { -// id = sampleId -// parentId = nil -// userId = 10 -// userRole = .Student -// time = NSDate() -// lastTime = NSDate() -// -// let latexStrings = [ -// "Here is a simple LaTeX $x^2 + 3*x - 10/(y*z^3)$", -// "A bit easier $x$ and it became really long long long long looooooong long long long long", -// "The best string with LaTeX $(x*a*b + 2*x/(z^2))/(200*y^6 + x/z)$", -// ] -// -// text = latexStrings[min(sampleId, 2)] -// replyCount = 0 -// isDeleted = false -// targetStepId = 0 -// repliesIds = [] -// isPinned = false -// } -} - -struct CommentPostable { - var parent: Int? - var target: Int - var text: String - init(parent: Int? = nil, target: Int, text: String) { - self.parent = parent - self.target = target + self.parentId = parent + self.targetStepId = target self.text = text } - var json: [String: AnyObject] { - var dict: [String: AnyObject] = [ - "target": target as AnyObject, - "text": text as AnyObject + var json: JSON { + var dict: JSON = [ + "target": targetStepId, + "text": text ] - if let p = parent { - dict["parent"] = p as AnyObject? + if let parent = parentId { + try! dict.merge(with: ["parent": parent]) } return dict diff --git a/Stepic/CommentsAPI.swift b/Stepic/CommentsAPI.swift index 922086f982..7898012c06 100644 --- a/Stepic/CommentsAPI.swift +++ b/Stepic/CommentsAPI.swift @@ -9,41 +9,16 @@ import Foundation import Alamofire import SwiftyJSON +import PromiseKit -class CommentsAPI { - - let name: String = "comments" - - @discardableResult func retrieve(_ ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ([Comment]) -> Void, error errorHandler: @escaping (String) -> Void) -> Request { - let idsString = ApiUtil.constructIdsString(array: ids) - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/\(name)?\(idsString)", headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } - let response = response.response - - if let e = error as NSError? { - errorHandler("RETRIEVE comments: error \(e.localizedDescription)") - return - } - - if response?.statusCode != 200 { - errorHandler("RETRIEVE comments: bad response status code \(String(describing: response?.statusCode))") - return - } - - let comments: [Comment] = json["comments"].arrayValue.flatMap { - Comment(json: $0) - } +class CommentsAPI: APIEndpoint { + override var name: String { return "comments" } + func retrieve(ids: [Int]) -> Promise<[Comment]> { + return Promise { + fulfill, reject in + retrieve.request(requestEndpoint: "comments", paramName: "comments", ids: ids, updating: Array(), withManager: manager).then { + comments, json -> Void in var usersDict: [Int : UserInfo] = [Int: UserInfo]() json["users"].arrayValue.forEach { @@ -62,68 +37,51 @@ class CommentsAPI { comment.userInfo = usersDict[comment.userId] comment.vote = votesDict[comment.voteId] } - - success(comments) + fulfill(comments) + }.catch { + error in + reject(error) } - ) + } } - @discardableResult func create(_ comment: CommentPostable, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping (Comment) -> Void, error errorHandler: @escaping (String) -> Void) -> Request { - let params: Parameters = [ - "comment": comment.json - ] - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/\(name)", method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } - let response = response.response - - if let e = error as NSError? { - errorHandler("CREATE comments: error \(e.domain) \(e.code): \(e.localizedDescription)") + func create(_ comment: Comment) -> Promise { + return Promise { fulfill, reject in + create.request(requestEndpoint: "comments", paramName: "comment", creatingObject: comment, withManager: manager).then { + comment, json -> Void in + guard let json = json else { + fulfill(comment) return } - - if response?.statusCode != 201 { - errorHandler("CREATE comments: bad response status code \(String(describing: response?.statusCode))") - return - } - - let comment: Comment = Comment(json: json["comments"].arrayValue[0]) let userInfo = UserInfo(json: json["users"].arrayValue[0]) let vote = Vote(json: json["votes"].arrayValue[0]) comment.userInfo = userInfo comment.vote = vote - - success(comment) + fulfill(comment) + }.catch { + error in + reject(error) } - ) + } } } -struct UserInfo { - var id: Int - var avatarURL: String - var firstName: String - var lastName: String - init(json: JSON) { - id = json["id"].intValue - avatarURL = json["avatar"].stringValue - firstName = json["first_name"].stringValue - lastName = json["last_name"].stringValue +extension CommentsAPI { + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func create(_ comment: Comment, success: @escaping (Comment) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { + create(comment).then { success($0) }.catch { errorHandler($0.localizedDescription) } + return nil } - init(sample: Bool) { - id = 10 - avatarURL = "http://google.com/" - firstName = "Sample" - lastName = "User" + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func retrieve(_ ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ([Comment]) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { + retrieve(ids: ids).then { + comments in + success(comments) + }.catch { + error in + errorHandler(error.localizedDescription) + } + return nil } } diff --git a/Stepic/Course.swift b/Stepic/Course.swift index 23e20650eb..57d8059547 100644 --- a/Stepic/Course.swift +++ b/Stepic/Course.swift @@ -12,11 +12,11 @@ import SwiftyJSON import PromiseKit @objc -class Course: NSManagedObject, JSONInitializable { +final class Course: NSManagedObject, IDFetchable { // Insert code here to add functionality to your managed object subclass - typealias idType = Int + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -58,10 +58,6 @@ class Course: NSManagedObject, JSONInitializable { } } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } - var metaInfo: String { //percent of completion = n_steps_passed/n_steps if let p = self.progress { diff --git a/Stepic/CourseList.swift b/Stepic/CourseList.swift index 3b70abf39f..138aef15d4 100644 --- a/Stepic/CourseList.swift +++ b/Stepic/CourseList.swift @@ -11,8 +11,8 @@ import CoreData import SwiftyJSON import PromiseKit -class CourseList: NSManagedObject, JSONInitializable { - typealias idType = Int +final class CourseList: NSManagedObject, IDFetchable { + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -32,10 +32,6 @@ class CourseList: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].int - } - var language: ContentLanguage { return ContentLanguage(languageString: languageString) } diff --git a/Stepic/CourseListPresenter.swift b/Stepic/CourseListPresenter.swift index d77bd5845d..ada58f671a 100644 --- a/Stepic/CourseListPresenter.swift +++ b/Stepic/CourseListPresenter.swift @@ -305,8 +305,6 @@ class CourseListPresenter { func refresh() { displayCachedAsyncIfEmpty().then { self.updateState() - }.then { - checkToken() }.then { [weak self] in self?.refreshCourses() diff --git a/Stepic/CourseListsAPI.swift b/Stepic/CourseListsAPI.swift index 1fe15e67aa..5d21efdc93 100644 --- a/Stepic/CourseListsAPI.swift +++ b/Stepic/CourseListsAPI.swift @@ -16,43 +16,12 @@ class CourseListsAPI: APIEndpoint { return "course-lists" } - @discardableResult func retrieve(language: ContentLanguage, page: Int = 1, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise<([CourseList], Meta)> { + func retrieve(language: ContentLanguage, page: Int = 1) -> Promise<([CourseList], Meta)> { let params : Parameters = [ "platform": "mobile", "language": language.languageString, "page": page ] - - return Promise<([CourseList], Meta)> { - fulfill, reject in - manager.request("\(StepicApplicationsInfo.apiURL)/\(name)", method: .get, parameters: params, headers: headers).validate().responseSwiftyJSON { response in - switch response.result { - - case .failure(let error): - reject(RetrieveError(error: error)) - - case .success(let json): - let meta = Meta(json: json["meta"]) - //TODO: Better make this recovery-update mechanism more generic to avoid code duplication. Think about it. - let jsonArray: [JSON] = json["course-lists"].array ?? [] - let listIds: [Int] = jsonArray.map { - $0["id"].intValue - } - let recoveredLists = CourseList.recover(ids: listIds) - let resultArray: [CourseList] = jsonArray.map { - objectJSON in - if let recoveredIndex = recoveredLists.index(where: { $0.hasEqualId(json: objectJSON) }) { - recoveredLists[recoveredIndex].update(json: objectJSON) - return recoveredLists[recoveredIndex] - } else { - return CourseList(json: objectJSON) - } - } - - CoreDataHelper.instance.save() - fulfill((resultArray, meta)) - } - } - } + return retrieve.requestWithFetching(requestEndpoint: "course-lists", paramName: "course-lists", params: params, withManager: manager) } } diff --git a/Stepic/CourseReviewSummariesAPI.swift b/Stepic/CourseReviewSummariesAPI.swift index d0bb9d4820..8b5e34761e 100644 --- a/Stepic/CourseReviewSummariesAPI.swift +++ b/Stepic/CourseReviewSummariesAPI.swift @@ -14,6 +14,6 @@ class CourseReviewSummariesAPI: APIEndpoint { override var name: String { return "course-review-summaries" } @discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [CourseReviewSummary], refreshMode: RefreshMode, success: @escaping (([CourseReviewSummary]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { - return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) + return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) } } diff --git a/Stepic/CourseReviewSummary.swift b/Stepic/CourseReviewSummary.swift index ebcc28441b..e498511a40 100644 --- a/Stepic/CourseReviewSummary.swift +++ b/Stepic/CourseReviewSummary.swift @@ -10,9 +10,9 @@ import Foundation import CoreData import SwiftyJSON -class CourseReviewSummary: NSManagedObject, JSONInitializable { +class CourseReviewSummary: NSManagedObject, JSONSerializable { - typealias idType = Int + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -29,8 +29,4 @@ class CourseReviewSummary: NSManagedObject, JSONInitializable { func update(json: JSON) { initialize(json) } - - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } } diff --git a/Stepic/CoursesAPI.swift b/Stepic/CoursesAPI.swift index 686cefcb0f..2d2feaee9e 100644 --- a/Stepic/CoursesAPI.swift +++ b/Stepic/CoursesAPI.swift @@ -14,72 +14,11 @@ import PromiseKit class CoursesAPI: APIEndpoint { override var name: String { return "courses" } - @discardableResult func retrieveDisplayedIds(featured: Bool?, enrolled: Bool?, isPublic: Bool?, order: String?, page: Int?, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success : @escaping ([Int], Meta) -> Void, failure : @escaping (_ error: Error) -> Void) -> Request? { - - var params = Parameters() - - if let f = featured { - params["is_featured"] = f ? "true" : "false" - } - - if let e = enrolled { - params["enrolled"] = e ? "true" : "false" - } - - if let p = isPublic { - params["is_public"] = p ? "true" : "false" - } - - if let o = order { - params["order"] = o - } - - if let p = page { - params["page"] = p - } - - params["access_token"] = AuthInfo.shared.token?.accessToken as NSObject? - - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/courses", parameters: params, encoding: URLEncoding.default, headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } -// let response = response.response - - //TODO: Remove from here - if let e = error { - print(e) - failure(e) - return - } - - let meta = Meta(json: json["meta"]) - var res: [Int] = [] - - for objectJSON in json["courses"].arrayValue { - res += [objectJSON["id"].intValue] - } - success(res, meta) - }) - } - - @discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [Course], refreshMode: RefreshMode, success: @escaping (([Course]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { - return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) - } - @discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [Course]) -> Promise<[Course]> { return getObjectsByIds(ids: ids, updating: existing) } - @discardableResult func retrieve(tag: Int? = nil, featured: Bool? = nil, enrolled: Bool? = nil, excludeEnded: Bool? = nil, isPublic: Bool? = nil, order: String? = nil, language: ContentLanguage? = nil, page: Int = 1, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success successHandler: @escaping ([Course], Meta) -> Void, error errorHandler: @escaping (Error) -> Void) -> Request? { + func retrieve(tag: Int? = nil, featured: Bool? = nil, enrolled: Bool? = nil, excludeEnded: Bool? = nil, isPublic: Bool? = nil, order: String? = nil, language: ContentLanguage? = nil, page: Int = 1) -> Promise<([Course], Meta)> { var params = Parameters() if let isFeatured = featured { @@ -112,49 +51,27 @@ class CoursesAPI: APIEndpoint { params["page"] = page - return manager.request("\(StepicApplicationsInfo.apiURL)/\(name)", parameters: params, encoding: URLEncoding.default, headers: headers).validate().responseSwiftyJSON({ response in - switch response.result { - - case .failure(let error): - errorHandler(error) - return - case .success(let json): - // get courses ids - let jsonArray: [JSON] = json["courses"].array ?? [] - let ids: [Int] = jsonArray.flatMap { $0["id"].int } - // recover course objects from database - let recoveredCourses = Course.getCourses(ids) - // update existing course objects or create new ones - let resultCourses: [Course] = ids.enumerated().map { - idIndex, id in - let jsonObject = jsonArray[idIndex] - if let recoveredCourseIndex = recoveredCourses.index(where: {$0.id == id}) { - recoveredCourses[recoveredCourseIndex].update(json: jsonObject) - return recoveredCourses[recoveredCourseIndex] - } else { - return Course(json: jsonObject) - } - } - - CoreDataHelper.instance.save() - let meta = Meta(json: json["meta"]) - - successHandler(resultCourses, meta) - return - } - }) + return retrieve.requestWithFetching(requestEndpoint: "courses", paramName: "courses", params: params, withManager: manager) } - //Could wrap retrieveDisplayedIds - @discardableResult func retrieve(tag: Int? = nil, featured: Bool? = nil, enrolled: Bool? = nil, excludeEnded: Bool? = nil, isPublic: Bool? = nil, order: String? = nil, language: ContentLanguage? = nil, page: Int = 1, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise<([Course], Meta)> { - return Promise { fulfill, reject in - retrieve(tag: tag, featured: featured, enrolled: enrolled, excludeEnded: excludeEnded, isPublic: isPublic, order: order, language: language, page: page, headers: headers, success: { - courses, meta in - fulfill((courses, meta)) - }, error: { - error in - reject(error) - }) + //Can't add this to extension because it is mocked in tests. "Declaration from extension cannot be overriden" + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [Course], refreshMode: RefreshMode, success: @escaping (([Course]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { + return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) + } +} + +extension CoursesAPI { + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func retrieve(tag: Int? = nil, featured: Bool? = nil, enrolled: Bool? = nil, excludeEnded: Bool? = nil, isPublic: Bool? = nil, order: String? = nil, language: ContentLanguage? = nil, page: Int = 1, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success successHandler: @escaping ([Course], Meta) -> Void, error errorHandler: @escaping (Error) -> Void) -> Request? { + retrieve(tag: tag, featured: featured, enrolled: enrolled, excludeEnded: excludeEnded, isPublic: isPublic, order: order, language: language, page: page).then { + courses, meta in + successHandler(courses, meta) + }.catch { + error in + errorHandler(error) } + return nil } + } diff --git a/Stepic/CreateRequestMaker.swift b/Stepic/CreateRequestMaker.swift new file mode 100644 index 0000000000..f3708b066f --- /dev/null +++ b/Stepic/CreateRequestMaker.swift @@ -0,0 +1,57 @@ +// +// CreateRequestMaker.swift +// Stepic +// +// Created by Ostrenkiy on 20.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import Foundation +import Alamofire +import PromiseKit +import SwiftyJSON + +class CreateRequestMaker { + func request(requestEndpoint: String, paramName: String, creatingObject: T, withManager manager: Alamofire.SessionManager) -> Promise<(T, JSON?)> { + return Promise { fulfill, reject in + let params: Parameters? = [ + paramName: creatingObject.json.dictionaryObject ?? "" + ] + + checkToken().then { + manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", method: .post, parameters: params, encoding: JSONEncoding.default).validate().responseSwiftyJSON { response in + switch response.result { + case .failure(let error): + reject(error) + case .success(let json): + creatingObject.update(json: json[requestEndpoint].arrayValue[0]) + fulfill((creatingObject, json)) + } + } + }.catch { + error in + reject(error) + } + } + } + + func request(requestEndpoint: String, paramName: String, creatingObject: T, withManager manager: Alamofire.SessionManager) -> Promise { + return Promise { fulfill, reject in + request(requestEndpoint: requestEndpoint, paramName: paramName, creatingObject: creatingObject, withManager: manager).then { comment, _ in + fulfill(comment) + }.catch { error in + reject(error) + } + } + } + + func request(requestEndpoint: String, paramName: String, creatingObject: T, withManager manager: Alamofire.SessionManager) -> Promise { + return Promise { fulfill, reject in + request(requestEndpoint: requestEndpoint, paramName: paramName, creatingObject: creatingObject, withManager: manager).then { _, _ in + fulfill(()) + }.catch { error in + reject(error) + } + } + } +} diff --git a/Stepic/DatabaseFetchService.swift b/Stepic/DatabaseFetchService.swift new file mode 100644 index 0000000000..7190d2cddc --- /dev/null +++ b/Stepic/DatabaseFetchService.swift @@ -0,0 +1,40 @@ +// +// DatabaseFetchService.swift +// Stepic +// +// Created by Ostrenkiy on 28.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import Foundation +import PromiseKit +import SwiftyJSON +import CoreData + +class DatabaseFetchService { + static func fetchAsync(entityName: String, ids: [T.IdType]) -> Promise<[T]> { + let request = NSFetchRequest(entityName: entityName) + let descriptor = NSSortDescriptor(key: "managedId", ascending: false) + + let idPredicates = ids.map { + NSPredicate(format: "managedId == %@", $0.fetchValue) + } + let predicate = NSCompoundPredicate(type: NSCompoundPredicate.LogicalType.or, subpredicates: idPredicates) + + request.predicate = predicate + request.sortDescriptors = [descriptor] + + return Promise<[T]> { + fulfill, _ in + let asyncRequest = NSAsynchronousFetchRequest(fetchRequest: request, completionBlock: { + results in + guard let courses = results.finalResult as? [T] else { + fulfill([]) + return + } + fulfill(courses) + }) + _ = try? CoreDataHelper.instance.context.execute(asyncRequest) + } + } +} diff --git a/Stepic/DeleteRequestMaker.swift b/Stepic/DeleteRequestMaker.swift new file mode 100644 index 0000000000..85c921454d --- /dev/null +++ b/Stepic/DeleteRequestMaker.swift @@ -0,0 +1,31 @@ +// +// DeleteRequestMaker.swift +// Stepic +// +// Created by Ostrenkiy on 20.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import Foundation +import Alamofire +import PromiseKit + +class DeleteRequestMaker { + func request(requestEndpoint: String, deletingId: Int, withManager manager: Alamofire.SessionManager) -> Promise { + return Promise { fulfill, reject in + checkToken().then { + manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(deletingId)", method: .delete, encoding: JSONEncoding.default).validate().responseSwiftyJSON { response in + switch response.result { + case .failure(let error): + reject(error) + case .success(_): + fulfill(()) + } + } + }.catch { + error in + reject(error) + } + } + } +} diff --git a/Stepic/DevicesAPI.swift b/Stepic/DevicesAPI.swift index 2070e082fb..3f46fa0adc 100644 --- a/Stepic/DevicesAPI.swift +++ b/Stepic/DevicesAPI.swift @@ -11,6 +11,7 @@ import Alamofire import SwiftyJSON import PromiseKit +//TODO: Refactor this after DeviceError refactoring class DevicesAPI: APIEndpoint { override var name: String { return "devices" } @@ -52,6 +53,7 @@ class DevicesAPI: APIEndpoint { } } + //TODO: Update this after errors refactoring. DeviceError is something that should be dealt with func update(_ device: Device, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise { return Promise { fulfill, reject in guard let deviceId = device.id else { @@ -83,6 +85,7 @@ class DevicesAPI: APIEndpoint { } } + //TODO: Update this after errors refactoring. DeviceError is something that should be dealt with func create(_ device: Device, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise { let params = ["device": device.json] @@ -108,6 +111,7 @@ class DevicesAPI: APIEndpoint { } } + //TODO: Update this after errors refactoring. DeviceError is something that should be dealt with func delete(_ deviceId: Int, headers: [String: String] = APIDefaults.headers.bearer) -> Promise { return Promise { fulfill, reject in manager.request("\(StepicApplicationsInfo.apiURL)/devices/\(deviceId)", method: .delete, headers: headers).responseSwiftyJSON { response in diff --git a/Stepic/DiscussionProxiesAPI.swift b/Stepic/DiscussionProxiesAPI.swift index bb532338cf..ed7a075762 100644 --- a/Stepic/DiscussionProxiesAPI.swift +++ b/Stepic/DiscussionProxiesAPI.swift @@ -9,40 +9,26 @@ import Foundation import Alamofire import SwiftyJSON +import PromiseKit -class DiscussionProxiesAPI { - let name = "discussion-proxies" +class DiscussionProxiesAPI: APIEndpoint { + override var name: String { return "discussion-proxies" } - @discardableResult func retrieve(_ id: String, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ((DiscussionProxy) -> Void), error errorHandler: @escaping ((String) -> Void)) -> Request { - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/\(name)/\(id)", headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } - let response = response.response - - if let e = error as NSError? { - errorHandler("RETRIEVE discussion-proxies/\(id): error \(e.domain) \(e.code): \(e.localizedDescription)") - return - } - - if response?.statusCode != 200 { - errorHandler("RETRIEVE discussion-proxies/\(id): bad response status code \(String(describing: response?.statusCode))") - return - } - - let discussionProxy = DiscussionProxy(json: json["discussion-proxies"].arrayValue[0]) - success(discussionProxy) + func retrieve(id: String) -> Promise { + return retrieve.request(requestEndpoint: "discussion-proxies", paramName: "discussion-proxies", id: id, withManager: manager) + } +} - return - } - ) +extension DiscussionProxiesAPI { + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func retrieve(_ id: String, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ((DiscussionProxy) -> Void), error errorHandler: @escaping ((String) -> Void)) -> Request? { + retrieve(id: id).then { + discussionProxy in + success(discussionProxy) + }.catch { + error in + errorHandler(error.localizedDescription) + } + return nil } } diff --git a/Stepic/DiscussionProxy.swift b/Stepic/DiscussionProxy.swift index 2f68431364..fd894806dc 100644 --- a/Stepic/DiscussionProxy.swift +++ b/Stepic/DiscussionProxy.swift @@ -9,12 +9,15 @@ import Foundation import SwiftyJSON -class DiscussionProxy { +class DiscussionProxy: JSONSerializable { + var discussionIds: [Int] = [] + var id: String = "" - var discussionIds: [Int] - var id: String + required init(json: JSON) { + update(json: json) + } - init(json: JSON) { + func update(json: JSON) { discussionIds = json["discussions"].arrayValue.flatMap { $0.int } diff --git a/Stepic/EnrollmentsAPI.swift b/Stepic/EnrollmentsAPI.swift index 1c1180b042..154fe2f424 100644 --- a/Stepic/EnrollmentsAPI.swift +++ b/Stepic/EnrollmentsAPI.swift @@ -11,8 +11,8 @@ import SwiftyJSON import Alamofire import PromiseKit -class EnrollmentsAPI { - let name = "enrollments" +class EnrollmentsAPI: APIEndpoint { + override var name: String { return "enrollments" } func joinCourse(_ course: Course, delete: Bool = false) -> Promise { return Promise { fulfill, reject in @@ -24,6 +24,11 @@ class EnrollmentsAPI { } } + func delete(courseId: Int) -> Promise { + return delete.request(requestEndpoint: "enrollments", deletingId: courseId, withManager: manager) + } + + //TODO: Refactor this to create() and delete() methods @discardableResult func joinCourse(_ course: Course, delete: Bool = false, success : @escaping () -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { let headers: [String : String] = AuthInfo.shared.initialHTTPHeaders diff --git a/Stepic/ExplorePresenter.swift b/Stepic/ExplorePresenter.swift index ce8ea29414..39c476a9d2 100644 --- a/Stepic/ExplorePresenter.swift +++ b/Stepic/ExplorePresenter.swift @@ -260,19 +260,8 @@ class ExplorePresenter: CourseListCountDelegate { } private func refreshFromRemote(forLanguage language: ContentLanguage) { - checkToken().then { - [weak self] - () -> Promise<([CourseList], Meta)> in - guard let strongSelf = self else { - throw WeakSelfError.noStrong - } - if ContentLanguage.sharedContentLanguage != language { - throw LanguageError.wrongLanguageError - } - - strongSelf.didRefreshOnce = true - return strongSelf.courseListsAPI.retrieve(language: language, page: 1) - }.then { + didRefreshOnce = true + courseListsAPI.retrieve(language: language, page: 1).then { [weak self] lists, _ -> Void in guard let strongSelf = self else { diff --git a/Stepic/IDFetchable.swift b/Stepic/IDFetchable.swift new file mode 100644 index 0000000000..62091b6453 --- /dev/null +++ b/Stepic/IDFetchable.swift @@ -0,0 +1,49 @@ +// +// IDFetchable.swift +// Stepic +// +// Created by Ostrenkiy on 02.04.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import Foundation +import SwiftyJSON +import PromiseKit + +protocol IDFetchable: JSONSerializable where IdType: CoreDataRepresentable { + + static func getId(json: JSON) -> IdType? + static func fetchAsync(ids: [IdType]) -> Promise<[Self]> +} + +extension IDFetchable { + static func getId(json: JSON) -> IdType? { + if IdType.self == Int.self { + return json["id"].int as? Self.IdType + } + if IdType.self == String.self { + return json["id"].string as? Self.IdType + } + return nil + } + + static func fetchAsync(ids: [IdType]) -> Promise<[Self]> { + return DatabaseFetchService.fetchAsync(entityName: String(describing: Self.self), ids: ids) + } +} + +protocol CoreDataRepresentable { + var fetchValue: CVarArg { get } +} + +extension String: CoreDataRepresentable { + var fetchValue: CVarArg { + return self + } +} + +extension Int: CoreDataRepresentable { + var fetchValue: CVarArg { + return self as NSNumber + } +} diff --git a/Stepic/JSONInitializable.swift b/Stepic/JSONInitializable.swift deleted file mode 100644 index 050fb18613..0000000000 --- a/Stepic/JSONInitializable.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// JSONInitializable.swift -// Stepic -// -// Created by Alexander Karpov on 09.10.15. -// Copyright © 2015 Alex Karpov. All rights reserved. -// - -import UIKit -import SwiftyJSON - -protocol JSONInitializable { - - associatedtype idType: Equatable - - init(json: JSON) - func update(json: JSON) - - var id: idType {get set} - - func hasEqualId(json: JSON) -> Bool -} diff --git a/Stepic/JSONSerializable.swift b/Stepic/JSONSerializable.swift new file mode 100644 index 0000000000..37a7acc750 --- /dev/null +++ b/Stepic/JSONSerializable.swift @@ -0,0 +1,40 @@ +// +// JSONSerializable.swift +// Stepic +// +// Created by Alexander Karpov on 09.10.15. +// Copyright © 2015 Alex Karpov. All rights reserved. +// + +import UIKit +import SwiftyJSON +import PromiseKit + +protocol JSONSerializable { + + associatedtype IdType: Equatable + + init(json: JSON) + func update(json: JSON) + + var id: IdType {get set} + var json: JSON { get } + + func hasEqualId(json: JSON) -> Bool +} + +extension JSONSerializable { + func hasEqualId(json: JSON) -> Bool { + if IdType.self == Int.self { + return (json["id"].int as? Self.IdType) == self.id + } + if IdType.self == String.self { + return (json["id"].string as? Self.IdType) == self.id + } + return false + } + + var json: JSON { + return [] + } +} diff --git a/Stepic/LastStep.swift b/Stepic/LastStep.swift index db10d1f659..bb60c0b167 100644 --- a/Stepic/LastStep.swift +++ b/Stepic/LastStep.swift @@ -10,9 +10,9 @@ import Foundation import CoreData import SwiftyJSON -class LastStep: NSManagedObject, JSONInitializable { +class LastStep: NSManagedObject, JSONSerializable { - typealias idType = String + typealias IdType = String convenience required init(json: JSON) { self.init() @@ -40,9 +40,4 @@ class LastStep: NSManagedObject, JSONInitializable { func update(json: JSON) { initialize(json) } - - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].stringValue - } - } diff --git a/Stepic/LastStepsAPI.swift b/Stepic/LastStepsAPI.swift index d273b996e2..a72d9331c4 100644 --- a/Stepic/LastStepsAPI.swift +++ b/Stepic/LastStepsAPI.swift @@ -14,7 +14,6 @@ class LastStepsAPI: APIEndpoint { override var name: String { return "last-steps" } @discardableResult func retrieve(ids: [String], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, updatingLastSteps: [LastStep], success: @escaping (([LastStep]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { - return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: updatingLastSteps, refreshMode: .update, success: success, failure: errorHandler) - + return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: updatingLastSteps, refreshMode: .update, success: success, failure: errorHandler) } } diff --git a/Stepic/Lesson.swift b/Stepic/Lesson.swift index 95bbdbaec7..ab4eeed7ba 100644 --- a/Stepic/Lesson.swift +++ b/Stepic/Lesson.swift @@ -10,10 +10,10 @@ import Foundation import CoreData import SwiftyJSON -class Lesson: NSManagedObject, JSONInitializable { +class Lesson: NSManagedObject, JSONSerializable { // Insert code here to add functionality to your managed object subclass - typealias idType = Int + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -51,10 +51,6 @@ class Lesson: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } - func loadSteps(completion: @escaping (() -> Void), error errorHandler: ((String) -> Void)? = nil, onlyLesson: Bool = false) { _ = ApiDataDownloader.steps.retrieve(ids: self.stepsArray, existing: self.steps, refreshMode: .update, success: { newSteps in diff --git a/Stepic/LessonsAPI.swift b/Stepic/LessonsAPI.swift index 88a81297ee..c2d43022fc 100644 --- a/Stepic/LessonsAPI.swift +++ b/Stepic/LessonsAPI.swift @@ -15,7 +15,7 @@ class LessonsAPI: APIEndpoint { override var name: String { return "lessons" } func retrieve(ids: [Int], existing: [Lesson], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise<[Lesson]> { - return getObjectsByIds(ids: ids, updating: existing, headers: headers, printOutput: false) + return getObjectsByIds(ids: ids, updating: existing, printOutput: false) } } diff --git a/Stepic/Model.xcdatamodeld/Model_courselists_v21.xcdatamodel/contents b/Stepic/Model.xcdatamodeld/Model_courselists_v21.xcdatamodel/contents index 2acf5b892a..aabc893e36 100644 --- a/Stepic/Model.xcdatamodeld/Model_courselists_v21.xcdatamodel/contents +++ b/Stepic/Model.xcdatamodeld/Model_courselists_v21.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Stepic/Notification.swift b/Stepic/Notification.swift index 4ae796bd6e..000a6e6d6d 100644 --- a/Stepic/Notification.swift +++ b/Stepic/Notification.swift @@ -9,9 +9,11 @@ import Foundation import CoreData import SwiftyJSON +import PromiseKit -class Notification: NSManagedObject, JSONInitializable { - typealias idType = Int +final class Notification: NSManagedObject, JSONSerializable, IDFetchable { + + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -37,12 +39,8 @@ class Notification: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].int - } - - var json: [String: AnyObject] { - let dict: [String: AnyObject] = [ + var json: JSON { + return [ "id": id as AnyObject, "html_text": htmlText as AnyObject, "is_unread": (status == .unread) as AnyObject, @@ -53,7 +51,6 @@ class Notification: NSManagedObject, JSONInitializable { "level": level as AnyObject, "priority": priority as AnyObject ] - return dict } } diff --git a/Stepic/NotificationRegistrator.swift b/Stepic/NotificationRegistrator.swift index a51ac10a58..645d4cf6a4 100644 --- a/Stepic/NotificationRegistrator.swift +++ b/Stepic/NotificationRegistrator.swift @@ -67,6 +67,7 @@ class NotificationRegistrator { let newDevice = Device(registrationId: registrationToken, deviceDescription: DeviceInfo.current.deviceInfoString) + //TODO: Remove this after refactoring errors checkToken().then { _ -> Promise in if let savedDeviceId = DeviceDefaults.sharedDefaults.deviceId, !forceCreation { print("notification registrator: retrieve device by saved deviceId = \(savedDeviceId)") diff --git a/Stepic/NotificationStatusesAPI.swift b/Stepic/NotificationStatusesAPI.swift index aef739b89b..99a2d39acc 100644 --- a/Stepic/NotificationStatusesAPI.swift +++ b/Stepic/NotificationStatusesAPI.swift @@ -8,20 +8,23 @@ import Foundation import PromiseKit +import Alamofire class NotificationStatusesAPI: APIEndpoint { override var name: String { return "notification-statuses" } func retrieve() -> Promise { return Promise { fulfill, reject in - manager.request("\(StepicApplicationsInfo.apiURL)/\(name)", parameters: nil, headers: AuthInfo.shared.initialHTTPHeaders).responseSwiftyJSON { response in - switch response.result { - case .failure(let error): - reject(RetrieveError(error: error)) - case .success(let json): - let ns = NotificationsStatus(json: json["notification-statuses"].arrayValue[0]) - fulfill(ns) + retrieve.request(requestEndpoint: "notification-statuses", paramName: "notification-statuses", params: Parameters(), updatingObjects: Array(), withManager: manager).then { + notificationStatuses, _, _ -> Void in + guard let status = notificationStatuses.first else { + reject(RetrieveError.parsingError) + return } + fulfill(status) + }.catch { + error in + reject(error) } } } diff --git a/Stepic/NotificationsAPI.swift b/Stepic/NotificationsAPI.swift index 773e684628..0b49fc6435 100644 --- a/Stepic/NotificationsAPI.swift +++ b/Stepic/NotificationsAPI.swift @@ -16,76 +16,41 @@ import PromiseKit class NotificationsAPI: APIEndpoint { override var name: String { return "notifications" } - func retrieve(page: Int = 1, notificationType: NotificationType? = nil, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise<(Meta, [Notification])> { - return Promise { fulfill, reject in - var parameters = [ - "page": "\(page)" - ] - - if let notificationType = notificationType { - parameters["type"] = notificationType.rawValue - } - - manager.request("\(StepicApplicationsInfo.apiURL)/\(self.name)", parameters: parameters, headers: headers).responseSwiftyJSON { response in - switch response.result { - case .failure(let error): - reject(RetrieveError(error: error)) - case .success(let json): - let savedNotifications = Notification.fetch(json["notifications"].arrayValue.map { $0["id"].intValue }) - var newNotifications: [Notification] = [] - for objectJSON in json["notifications"].arrayValue { - let existing = savedNotifications.filter { obj in obj.hasEqualId(json: objectJSON) } + func retrieve(page: Int = 1, notificationType: NotificationType? = nil) -> Promise<( [Notification], Meta)> { - switch existing.count { - case 0: - newNotifications.append(Notification(json: objectJSON)) - default: - let obj = existing[0] - obj.update(json: objectJSON) - newNotifications.append(obj) - } - } + var parameters = [ + "page": "\(page)" + ] - CoreDataHelper.instance.save() - - let meta = Meta(json: json["meta"]) - fulfill((meta, newNotifications)) - } - } + if let notificationType = notificationType { + parameters["type"] = notificationType.rawValue } - } - func update(_ notification: Notification, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise { - return Promise { fulfill, reject in - let params: Parameters? = [ - "notification": notification.json as AnyObject - ] + return retrieve.requestWithFetching(requestEndpoint: "notifications", paramName: "notifications", params: parameters, withManager: manager) + } - manager.request("\(StepicApplicationsInfo.apiURL)/\(self.name)/\(notification.id)", method: .put, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON { response in - switch response.result { - case .failure(let error): - reject(error) // raw error here - case .success(let json): - notification.update(json: json["notifications"].arrayValue[0]) - fulfill(notification) - } - } - } + func update(_ notification: Notification) -> Promise { + return update.request(requestEndpoint: "notifications", paramName: "notification", updatingObject: notification, withManager: manager) } func markAllAsRead(headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise<()> { return Promise { fulfill, reject in - manager.request("\(StepicApplicationsInfo.apiURL)/\(name)/mark-as-read", method: .post, parameters: nil, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON { response in - switch response.result { - case .failure(let error): - reject(error) - case .success(_): - if response.response?.statusCode != 204 { - reject(NotificationsAPIError.invalidStatus) - } else { - fulfill(()) + checkToken().then { + self.manager.request("\(StepicApplicationsInfo.apiURL)/\(self.name)/mark-as-read", method: .post, parameters: nil, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON { response in + switch response.result { + case .failure(let error): + reject(error) + case .success(_): + if response.response?.statusCode != 204 { + reject(NotificationsAPIError.invalidStatus) + } else { + fulfill(()) + } } } + }.catch { + error in + reject(error) } } } diff --git a/Stepic/NotificationsPresenter.swift b/Stepic/NotificationsPresenter.swift index 8915eed2c4..22c6241559 100644 --- a/Stepic/NotificationsPresenter.swift +++ b/Stepic/NotificationsPresenter.swift @@ -226,9 +226,7 @@ class NotificationsPresenter { fileprivate func loadData(page: Int, in section: NotificationsSection) -> Promise<(Bool, NotificationViewDataStruct)> { return Promise { fulfill, reject in var hasNext: Bool = false - checkToken().then { _ -> Promise<(Meta, [Notification])> in - self.notificationsAPI.retrieve(page: page, notificationType: section.notificationType) - }.then { meta, result -> Promise in + notificationsAPI.retrieve(page: page, notificationType: section.notificationType).then { result, meta -> Promise in hasNext = meta.hasNext return self.merge(old: self.displayedNotifications, new: result) @@ -303,9 +301,7 @@ class NotificationsPresenter { // Try to load user avatars and group notifications return Promise { fulfill, _ in - checkToken().then { _ -> Promise<[User]> in - self.usersAPI.retrieve(ids: Array(usersQuery), existing: []) - }.then { users -> Void in + usersAPI.retrieve(ids: Array(usersQuery), existing: []).then { users -> Void in users.forEach { user in userAvatars[user.id] = URL(string: user.avatarURL) } @@ -323,9 +319,7 @@ class NotificationsPresenter { } notification.status = status - checkToken().then { _ -> Promise in - self.notificationsAPI.update(notification) - }.then { _ -> Void in + self.notificationsAPI.update(notification).then { _ -> Void in CoreDataHelper.instance.save() NotificationCenter.default.post(name: .notificationUpdated, object: self, userInfo: ["section": self.section, "id": id, "status": status]) }.catch { error in @@ -336,9 +330,7 @@ class NotificationsPresenter { func markAllAsRead() { view?.updateMarkAllAsReadButton(with: .loading) - checkToken().then { _ -> Promise<()> in - self.notificationsAPI.markAllAsRead() - }.then { _ -> Void in + notificationsAPI.markAllAsRead().then { _ -> Void in Notification.markAllAsRead() AnalyticsReporter.reportEvent(AnalyticsEvents.Notifications.markAllAsRead, parameters: ["badge": self.badgeUnreadCount]) diff --git a/Stepic/NotificationsStatus.swift b/Stepic/NotificationsStatus.swift index c14b0cbcd4..089d0eb6d3 100644 --- a/Stepic/NotificationsStatus.swift +++ b/Stepic/NotificationsStatus.swift @@ -9,7 +9,7 @@ import Foundation import SwiftyJSON -class NotificationsStatus: NSObject { +class NotificationsStatus: JSONSerializable { var id: Int var learnCount: Int var reviewCount: Int @@ -18,7 +18,17 @@ class NotificationsStatus: NSObject { var defaultCount: Int var totalCount: Int - init(json: JSON) { + func update(json: JSON) { + self.id = json["id"].intValue + self.learnCount = json["learn"].intValue + self.reviewCount = json["review"].intValue + self.commentsCount = json["comments"].intValue + self.teachCount = json["teach"].intValue + self.defaultCount = json["default"].intValue + self.totalCount = json["total"].intValue + } + + required init(json: JSON) { self.id = json["id"].intValue self.learnCount = json["learn"].intValue self.reviewCount = json["review"].intValue @@ -26,6 +36,5 @@ class NotificationsStatus: NSObject { self.teachCount = json["teach"].intValue self.defaultCount = json["default"].intValue self.totalCount = json["total"].intValue - super.init() } } diff --git a/Stepic/Profile.swift b/Stepic/Profile.swift index f9c8789270..5f40826527 100644 --- a/Stepic/Profile.swift +++ b/Stepic/Profile.swift @@ -10,8 +10,8 @@ import Foundation import CoreData import SwiftyJSON -class Profile: NSManagedObject, JSONInitializable { - typealias idType = Int +class Profile: NSManagedObject, JSONSerializable { + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -29,17 +29,12 @@ class Profile: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].int - } - - var json: [String: AnyObject] { - let dict: [String: AnyObject] = [ + var json: JSON { + return [ "id": id as AnyObject, "first_name": firstName as AnyObject, "last_name": lastName as AnyObject, "subscribed_for_mail": subscribedForMail as AnyObject ] - return dict } } diff --git a/Stepic/ProfilesAPI.swift b/Stepic/ProfilesAPI.swift index efd261546d..ce9fc7901e 100644 --- a/Stepic/ProfilesAPI.swift +++ b/Stepic/ProfilesAPI.swift @@ -15,25 +15,10 @@ class ProfilesAPI: APIEndpoint { override var name: String { return "profiles" } func retrieve(ids: [Int], existing: [Profile], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise<[Profile]> { - return getObjectsByIds(ids: ids, updating: existing, headers: headers) + return getObjectsByIds(ids: ids, updating: existing) } - func update(_ profile: Profile, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise { - return Promise { fulfill, reject in - let params: Parameters? = [ - "profile": profile.json as AnyObject - ] - - manager.request("\(StepicApplicationsInfo.apiURL)/\(self.name)/\(profile.id)", method: .put, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON { response in - switch response.result { - case .failure(let error): - reject(error) // raw error here - case .success(let json): - profile.update(json: json["profiles"].arrayValue[0]) - fulfill(profile) - } - } - } + func update(_ profile: Profile) -> Promise { + return update.request(requestEndpoint: "profiles", paramName: "profile", updatingObject: profile, withManager: manager) } - } diff --git a/Stepic/Progress.swift b/Stepic/Progress.swift index e6004621f9..acbbb02305 100644 --- a/Stepic/Progress.swift +++ b/Stepic/Progress.swift @@ -10,9 +10,9 @@ import Foundation import CoreData import SwiftyJSON -class Progress: NSManagedObject, JSONInitializable { +class Progress: NSManagedObject, JSONSerializable { - typealias idType = String + typealias IdType = String convenience required init(json: JSON) { self.init() @@ -29,12 +29,20 @@ class Progress: NSManagedObject, JSONInitializable { lastViewed = json["last_viewed"].doubleValue } - func update(json: JSON) { - initialize(json) + var json: JSON { + return [ + "id" : id, + "is_passed": isPassed, + "score": score, + "cost": cost, + "n_steps": numberOfSteps, + "n_steps_passed": numberOfStepsPassed, + "last_viewed": lastViewed + ] } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].stringValue + func update(json: JSON) { + initialize(json) } var percentPassed: Float { diff --git a/Stepic/ProgressesAPI.swift b/Stepic/ProgressesAPI.swift index 58a809b978..e0993dd2f0 100644 --- a/Stepic/ProgressesAPI.swift +++ b/Stepic/ProgressesAPI.swift @@ -14,6 +14,6 @@ class ProgressesAPI: APIEndpoint { override var name: String { return "progresses" } @discardableResult func retrieve(ids: [String], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [Progress], refreshMode: RefreshMode, success: @escaping (([Progress]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { - return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) + return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) } } diff --git a/Stepic/QueriesAPI.swift b/Stepic/QueriesAPI.swift index 2814bbdc53..e0d0beffae 100644 --- a/Stepic/QueriesAPI.swift +++ b/Stepic/QueriesAPI.swift @@ -9,9 +9,11 @@ import Foundation import Alamofire import SwiftyJSON +import PromiseKit -class QueriesAPI { - let name = "queries" +//TODO: Refactor this by adding class Query: JSONSerializable +class QueriesAPI: APIEndpoint { + override var name: String { return "queries" } @discardableResult func retrieve(query: String, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping (([String]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { diff --git a/Stepic/QuizPresenter.swift b/Stepic/QuizPresenter.swift index 9853568c12..6a1db0ffa6 100644 --- a/Stepic/QuizPresenter.swift +++ b/Stepic/QuizPresenter.swift @@ -127,8 +127,7 @@ class QuizPresenter { var attempt: Attempt? { didSet { guard let attempt = attempt, - let dataset = attempt.dataset, - let id = attempt.id else { + let dataset = attempt.dataset else { print("Attempt should never be nil") return } @@ -136,7 +135,7 @@ class QuizPresenter { self.state = .attempt self.view?.update(limit: submissionLimit) view?.display(dataset: dataset) - if let cachedReply = ReplyCache.shared.getReply(forStepId: step.id, attemptId: id) { + if let cachedReply = ReplyCache.shared.getReply(forStepId: step.id, attemptId: attempt.id) { view?.display(reply: cachedReply) } checkSubmissionRestrictions() @@ -198,7 +197,7 @@ class QuizPresenter { //Get submission for attempt let currentAttempt = attempts[0] s.attempt = currentAttempt - _ = s.submissionsAPI.retrieve(stepName: s.step.block.name, attemptId: currentAttempt.id!, success: { + _ = s.submissionsAPI.retrieve(stepName: s.step.block.name, attemptId: currentAttempt.id, success: { [weak self] submissions, _ in guard let s = self else { return } @@ -326,7 +325,7 @@ class QuizPresenter { } private func submit(reply: Reply, completion: @escaping (() -> Void), error errorHandler: @escaping ((String) -> Void)) { - let id = attempt!.id! + guard let id = attempt?.id else { return } performRequest({ [weak self] in guard let s = self else { return } @@ -343,7 +342,7 @@ class QuizPresenter { } s.submission = submission - s.checkSubmission(submission.id!, time: 0, completion: completion) + s.checkSubmission(submission.id, time: 0, completion: completion) }, error: { errorText in errorHandler(errorText) diff --git a/Stepic/RecommendationsAPI.swift b/Stepic/RecommendationsAPI.swift index e4d32d9406..60dc090f8e 100644 --- a/Stepic/RecommendationsAPI.swift +++ b/Stepic/RecommendationsAPI.swift @@ -11,6 +11,7 @@ import Alamofire import SwiftyJSON import PromiseKit +//TODO: Refactor this class into two separate API classes class RecommendationsAPI: APIEndpoint { override var name: String { return "recommendations" } var reactionName: String { return "recommendation-reactions" } diff --git a/Stepic/RetrieveRequestMaker.swift b/Stepic/RetrieveRequestMaker.swift new file mode 100644 index 0000000000..e11d62a6f5 --- /dev/null +++ b/Stepic/RetrieveRequestMaker.swift @@ -0,0 +1,180 @@ +// +// RetrieveRequestMaker.swift +// Stepic +// +// Created by Ostrenkiy on 27.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import Foundation +import Alamofire +import PromiseKit +import SwiftyJSON + +class RetrieveRequestMaker { + func request(requestEndpoint: String, paramName: String, id: T.IdType, updatingObject: T? = nil, withManager manager: Alamofire.SessionManager) -> Promise { + return Promise { fulfill, reject in + checkToken().then { + manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(id)", method: .get, encoding: URLEncoding.default).validate().responseSwiftyJSON { response in + switch response.result { + case .failure(let error): + reject(error) + case .success(let json): + if updatingObject != nil { + updatingObject?.update(json: json[paramName].arrayValue[0]) + } else { + fulfill(T(json: json[paramName].arrayValue[0])) + } + } + } + }.catch { + error in + reject(error) + } + } + } + + func request(requestEndpoint: String, paramName: String, params: Parameters, updatingObjects: [T], withManager manager: Alamofire.SessionManager) -> Promise<([T], Meta, JSON)> { + return Promise { fulfill, reject in + checkToken().then { + manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", method: .get, parameters: params, encoding: URLEncoding.default).validate().responseSwiftyJSON { response in + switch response.result { + case .failure(let error): + reject(error) + case .success(let json): + let jsonArray: [JSON] = json[paramName].array ?? [] + let resultArray: [T] = jsonArray.map { + objectJSON in + if let recoveredIndex = updatingObjects.index(where: { $0.hasEqualId(json: objectJSON) }) { + updatingObjects[recoveredIndex].update(json: objectJSON) + return updatingObjects[recoveredIndex] + } else { + return T(json: objectJSON) + } + } + let meta = Meta(json: json["meta"]) + fulfill((resultArray, meta, json)) + CoreDataHelper.instance.save() + } + } + }.catch { + error in + reject(error) + } + } + } + + func request(requestEndpoint: String, paramName: String, params: Parameters, updatingObjects: [T], withManager manager: Alamofire.SessionManager) -> Promise<([T], Meta)> { + return Promise { fulfill, reject in + request(requestEndpoint: requestEndpoint, paramName: paramName, params: params, updatingObjects: updatingObjects, withManager: manager).then { + objects, meta, _ in + fulfill((objects, meta)) + }.catch { + error in + reject(error) + } + } + } + + func requestWithFetching(requestEndpoint: String, paramName: String, params: Parameters, withManager manager: Alamofire.SessionManager) -> Promise<([T], Meta, JSON)> { + return Promise { fulfill, reject in + checkToken().then { + manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", method: .get, parameters: params, encoding: URLEncoding.default).validate().responseSwiftyJSON { response in + switch response.result { + case .failure(let error): + reject(error) + case .success(let json): + let ids = json[paramName].arrayValue.flatMap {T.getId(json: $0)} + T.fetchAsync(ids: ids).then { + existingObjects -> Void in + var resultArray: [T] = [] + for objectJSON in json[paramName].arrayValue { + let existing = existingObjects.filter { obj in obj.hasEqualId(json: objectJSON) } + + switch existing.count { + case 0: + resultArray.append(T(json: objectJSON)) + default: + let obj = existing[0] + obj.update(json: objectJSON) + resultArray.append(obj) + } + } + + CoreDataHelper.instance.save() + + let meta = Meta(json: json["meta"]) + fulfill((resultArray, meta, json)) + CoreDataHelper.instance.save() + }.catch { + error in + reject(error) + } + } + } + }.catch { + error in + reject(error) + } + } + } + + func requestWithFetching(requestEndpoint: String, paramName: String, params: Parameters, withManager manager: Alamofire.SessionManager) -> Promise<([T], Meta)> { + return Promise { fulfill, reject in + requestWithFetching(requestEndpoint: requestEndpoint, paramName: paramName, params: params, withManager: manager).then { + objects, meta, _ in + fulfill((objects, meta)) + }.catch { + error in + reject(error) + } + } + } + + func request(requestEndpoint: String, paramName: String, ids: [T.IdType], updating: [T], withManager manager: Alamofire.SessionManager) -> Promise<([T], JSON)> { + let params: Parameters = [ + "ids": ids + ] + return Promise { + fulfill, reject in + checkToken().then { + manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)", parameters: params, encoding: URLEncoding.default).validate().responseSwiftyJSON { response in + switch response.result { + + case .failure(let error): + reject(RetrieveError(error: error)) + + case .success(let json): + let jsonArray: [JSON] = json[paramName].array ?? [] + let resultArray: [T] = jsonArray.map { + objectJSON in + if let recoveredIndex = updating.index(where: { $0.hasEqualId(json: objectJSON) }) { + updating[recoveredIndex].update(json: objectJSON) + return updating[recoveredIndex] + } else { + return T(json: objectJSON) + } + } + + CoreDataHelper.instance.save() + fulfill((resultArray, json)) + } + } + } + } + } + + func request(requestEndpoint: String, paramName: String, ids: [T.IdType], updating: [T], withManager manager: Alamofire.SessionManager) -> Promise<[T]> { + return Promise { + fulfill, reject in + request(requestEndpoint: requestEndpoint, paramName: paramName, ids: ids, updating: updating, withManager: manager).then { + objects, _ in + fulfill(objects) + }.catch { + error in + reject(error) + } + } + } + +} diff --git a/Stepic/Section.swift b/Stepic/Section.swift index e136729f0f..208c80f56d 100644 --- a/Stepic/Section.swift +++ b/Stepic/Section.swift @@ -11,10 +11,10 @@ import CoreData import SwiftyJSON @objc -class Section: NSManagedObject, JSONInitializable { +class Section: NSManagedObject, JSONSerializable { // Insert code here to add functionality to your managed object subclass - typealias idType = Int + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -43,10 +43,6 @@ class Section: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } - class func getSections(_ id: Int) throws -> [Section] { let request = NSFetchRequest(entityName: "Section") diff --git a/Stepic/SectionsAPI.swift b/Stepic/SectionsAPI.swift index 85bcf98013..c80fc950a9 100644 --- a/Stepic/SectionsAPI.swift +++ b/Stepic/SectionsAPI.swift @@ -14,6 +14,6 @@ class SectionsAPI: APIEndpoint { override var name: String { return "sections" } @discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [Section], refreshMode: RefreshMode, success: @escaping (([Section]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { - return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) + return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) } } diff --git a/Stepic/Sorter.swift b/Stepic/Sorter.swift index 3cebc8e716..c1bb010e2d 100644 --- a/Stepic/Sorter.swift +++ b/Stepic/Sorter.swift @@ -10,7 +10,7 @@ import Foundation import SwiftyJSON struct Sorter { - static func sort(_ array: [T], byIds ids: [T.idType], canMissElements: Bool = false) -> [T] { + static func sort(_ array: [T], byIds ids: [T.IdType], canMissElements: Bool = false) -> [T] { var res: [T] = [] for id in ids { diff --git a/Stepic/Step.swift b/Stepic/Step.swift index 9e3b2d6902..24eec4d319 100644 --- a/Stepic/Step.swift +++ b/Stepic/Step.swift @@ -10,9 +10,9 @@ import Foundation import CoreData import SwiftyJSON -class Step: NSManagedObject, JSONInitializable { +class Step: NSManagedObject, JSONSerializable { - typealias idType = Int + typealias IdType = Int var canEdit: Bool = false @@ -57,10 +57,6 @@ class Step: NSManagedObject, JSONInitializable { block.update(json: json["block"]) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } - var hasReview: Bool = false static func getStepWithId(_ id: Int, unitId: Int? = nil) -> Step? { diff --git a/Stepic/StepicsAPI.swift b/Stepic/StepicsAPI.swift index 37dc7b3c4d..da602a9a2c 100644 --- a/Stepic/StepicsAPI.swift +++ b/Stepic/StepicsAPI.swift @@ -11,16 +11,8 @@ import Alamofire import SwiftyJSON import PromiseKit -class StepicsAPI { - let name = "stepics" - let manager: Alamofire.SessionManager - - init() { - let configuration = URLSessionConfiguration.default - configuration.timeoutIntervalForRequest = 15 - configuration.requestCachePolicy = .reloadIgnoringLocalCacheData - manager = Alamofire.SessionManager(configuration: configuration) - } +class StepicsAPI: APIEndpoint { + override var name: String { return "stepics" } func retrieveCurrentUser() -> Promise { return Promise { fulfill, reject in diff --git a/Stepic/StepikModelView.swift b/Stepic/StepikModelView.swift new file mode 100644 index 0000000000..d87afb841a --- /dev/null +++ b/Stepic/StepikModelView.swift @@ -0,0 +1,39 @@ +// +// StepikModelView.swift +// Stepic +// +// Created by Ostrenkiy on 21.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import Foundation +import SwiftyJSON + +class StepikModelView: JSONSerializable { + var id: Int = 0 + var step: Int = 0 + var assignment: Int? + + required init(json: JSON) { + update(json: json) + } + + func update(json: JSON) { + self.step = json["step"].intValue + self.assignment = json["assignment"].int + } + + var json: JSON { + var dict: JSON = ["step": step] + if let assignment = assignment { + try! dict.merge(with: ["assignment": assignment]) + } + return dict + } + + typealias IdType = Int + + func hasEqualId(json: JSON) -> Bool { + return false + } +} diff --git a/Stepic/StepsAPI.swift b/Stepic/StepsAPI.swift index 2beac9fdc1..c677fd487b 100644 --- a/Stepic/StepsAPI.swift +++ b/Stepic/StepsAPI.swift @@ -15,7 +15,7 @@ class StepsAPI: APIEndpoint { override var name: String { return "steps" } func retrieve(ids: [Int], existing: [Step], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise<[Step]> { - return getObjectsByIds(ids: ids, updating: existing, headers: headers, printOutput: false) + return getObjectsByIds(ids: ids, updating: existing, printOutput: false) } } diff --git a/Stepic/StepsControllerDeepLinkRouter.swift b/Stepic/StepsControllerDeepLinkRouter.swift index 432fc089b4..f1e3fbda86 100644 --- a/Stepic/StepsControllerDeepLinkRouter.swift +++ b/Stepic/StepsControllerDeepLinkRouter.swift @@ -109,9 +109,7 @@ class StepsControllerDeepLinkRouter: NSObject { } } - checkToken().then { _ -> Promise in - fetchOrLoadUnit(for: lesson) - }.then { unit -> Promise
in + fetchOrLoadUnit(for: lesson).then { unit -> Promise
in fetchOrLoadSection(for: unit) }.then { section -> Promise in fetchOrLoadCourse(for: section) diff --git a/Stepic/StepsControllerRouter.swift b/Stepic/StepsControllerRouter.swift deleted file mode 100644 index 0a0abc4817..0000000000 --- a/Stepic/StepsControllerRouter.swift +++ /dev/null @@ -1,176 +0,0 @@ -// -// StepsControllerRouter.swift -// Stepic -// -// Created by Alexander Karpov on 13.09.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - -import UIKit - -class StepsControllerRouter { - - //Getting step here - static func getStepsController(forStepId id: Int, success successHandler: @escaping ((LessonViewController) -> Void), error errorHandler: @escaping ((String) -> Void)) { - - let getForStepBlock: ((Step) -> Void) = { - step in - getStepsController(forStep: step, success: successHandler, error: errorHandler) - } - - if let step = Step.getStepWithId(id) { - if step.lesson != nil && step.lessonId == -1 { - step.lessonId = step.lesson!.id - } - - if step.lessonId != -1 { - getForStepBlock(step) - return - } - } - - ApiDataDownloader.steps.retrieve(ids: [id], existing: [], refreshMode: .update, success: { - steps in - if let step = steps.first { - getForStepBlock(step) - } else { - errorHandler("No step with id \(id)") - } - }, error: { - _ in - errorHandler("failed to get steps with id \(id)") - } - ) - - } - - //Getting lesson here - static func getStepsController(forStep step: Step, success successHandler: @escaping ((LessonViewController) -> Void), error errorHandler: @escaping ((String) -> Void)) { - let getForLessonBlock: ((Lesson) -> Void) = { - lesson in - getStepsController(forStep: step, lesson: lesson, success: successHandler, error: errorHandler) - } - - if let lesson = step.lesson { - getForLessonBlock(lesson) - return - } - - //TODO: Test this case. Check if additional downloads for other lesson's steps is needed. - //Possibly, this should really be done. - if let lesson = Lesson.getLesson(step.lessonId) { - step.lesson = lesson //Maybe it's a bad thing - getForLessonBlock(lesson) - return - } - - ApiDataDownloader.lessons.retrieve(ids: [step.lessonId], existing: [], refreshMode: .update, success: { - lessons in - if let lesson = lessons.first { - step.lesson = lesson //Maybe it's a bad thing - getForLessonBlock(lesson) - } else { - errorHandler("No lesson with id \(step.lessonId)") - } - - }, error: { - _ in - errorHandler("failed to get lesson with id \(step.lessonId)") - } - ) - - } - - //Getting unit here. - fileprivate static func getStepsController(forStep step: Step, lesson: Lesson, success successHandler: @escaping ((LessonViewController) -> Void), error errorHandler: @escaping ((String) -> Void)) { - - let getForUnitBlock: ((Unit) -> Void) = { - unit in - } - - //Lesson has unit, everything is OK - if let unit = lesson.unit { - getForUnitBlock(unit) - return - } - - //Check, if there is a unit for this lesson - ApiDataDownloader.units.retrieve(lesson: lesson.id, success: { - unit in - // there is a unit for lesson - unit.lesson = lesson - getForUnitBlock(unit) - return - }, error: { - error in - switch error { - case .noUnits: - //Handle the case, when there are no units - getStepsControllerForLessonContext(step, lesson: lesson, success: successHandler, error: errorHandler) - break - default: - errorHandler("Could not retrieve unit") - } - } - ) - } - - //Looking for assignments - fileprivate static func getStepsController(forStep step: Step, lesson: Lesson, unit: Unit, success successHandler: @escaping ((LessonViewController) -> Void), error errorHandler: @escaping ((String) -> Void)) { - - //Check, if cached assignments contain nil progresses -// unit.assignments.contains({$0.}) - - ApiDataDownloader.assignments.retrieve(ids: unit.assignmentsArray, existing: unit.assignments, refreshMode: .update, success: { - newAssignments in - - if newAssignments.count == 0 { - getStepsControllerForLessonContext(step, lesson: lesson, success: successHandler, error: errorHandler) - return - } - unit.assignments = Sorter.sort(newAssignments, byIds: unit.assignmentsArray) - - getStepsControllerForUnitContext(step, lesson: lesson, unit: unit, success: successHandler, error: errorHandler) - return - - }, error: { - _ in - errorHandler("Error while downloading assignments") - }) - - } - - //Define this method's signature later - fileprivate static func getStepsControllerForLessonContext(_ step: Step, lesson: Lesson, success successHandler: ((LessonViewController) -> Void), error errorHandler: ((String) -> Void)) { - - guard let vc = ControllerHelper.instantiateViewController(identifier: "LessonViewController") as? LessonViewController else { - errorHandler("Could not instantiate controller") - return - } - - vc.hidesBottomBarWhenPushed = true - let step = step - vc.initObjects = (lesson: lesson, startStepId: step.lesson?.steps.index(of: step) ?? 0, context: .lesson) - - successHandler(vc) - } - - //Define this method's signature later - fileprivate static func getStepsControllerForUnitContext(_ step: Step, lesson: Lesson, unit: Unit, success successHandler: ((LessonViewController) -> Void), error errorHandler: ((String) -> Void)) { - guard let vc = ControllerHelper.instantiateViewController(identifier: "LessonViewController") as? LessonViewController else { - errorHandler("Could not instantiate controller") - return - } - - vc.hidesBottomBarWhenPushed = true - let step = step - - //TODO: Add assignment here - //TODO: Check if it is better to do it using stepsArray - - vc.initObjects = (lesson: lesson, startStepId: step.lesson?.steps.index(of: step) ?? 0, context: .unit) - - successHandler(vc) - } - -} diff --git a/Stepic/Submission.swift b/Stepic/Submission.swift index 6fbaa5aa3a..34ad4c7835 100644 --- a/Stepic/Submission.swift +++ b/Stepic/Submission.swift @@ -9,23 +9,56 @@ import UIKit import SwiftyJSON -class Submission: NSObject { - var id: Int? +class Submission: JSONSerializable { + + typealias IdType = Int + + var id: Int = 0 var status: String? var reply: Reply? - var attempt: Int? + var attempt: Int = 0 var hint: String? init(json: JSON, stepName: String) { - id = json["id"].int + id = json["id"].intValue status = json["status"].string - attempt = json["attempt"].int + attempt = json["attempt"].intValue hint = json["hint"].string reply = nil - super.init() reply = getReplyFromJSON(json["reply"], stepName: stepName) } + init(attempt: Int, reply: Reply) { + self.attempt = attempt + self.reply = reply + } + + required init(json: JSON) { + update(json: json) + } + + func update(json: JSON) { + id = json["id"].intValue + status = json["status"].string + attempt = json["attempt"].intValue + hint = json["hint"].string + } + + func initReply(json: JSON, stepName: String) { + reply = getReplyFromJSON(json, stepName: stepName) + } + + func hasEqualId(json: JSON) -> Bool { + return id == json["id"].int + } + + var json: JSON { + return [ + "attempt": attempt, + "reply": reply?.dictValue ?? "" + ] + } + fileprivate func getReplyFromJSON(_ json: JSON, stepName: String) -> Reply? { switch stepName { case "choice" : return ChoiceReply(json: json) diff --git a/Stepic/SubmissionsAPI.swift b/Stepic/SubmissionsAPI.swift index 03c2f293a7..ef6da39dd8 100644 --- a/Stepic/SubmissionsAPI.swift +++ b/Stepic/SubmissionsAPI.swift @@ -9,6 +9,7 @@ import Foundation import Alamofire import SwiftyJSON +import PromiseKit class SubmissionsAPI: APIEndpoint { override var name: String { return "submissions" } @@ -105,45 +106,36 @@ class SubmissionsAPI: APIEndpoint { }) } - @discardableResult func create(stepName: String, attemptId: Int, reply: Reply, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping (Submission) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { - - let params: Parameters = [ - "submission": [ - "attempt": "\(attemptId)", - "reply": reply.dictValue - ] - ] - - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/submissions", method: .post, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() + func create(stepName: String, attemptId: Int, reply: Reply) -> Promise { + let submission = Submission(attempt: attemptId, reply: reply) + return Promise { fulfill, reject in + create.request(requestEndpoint: "submissions", paramName: "submission", creatingObject: submission, withManager: manager).then { + submission, json -> Void in + guard let json = json else { + fulfill(submission) + return } - } else { - json = response.result.value! - } - let response = response.response - - if let e = error { - let d = (e as NSError).localizedDescription - print(d) - errorHandler(d) - return + submission.initReply(json: json["submissions"].arrayValue[0]["reply"], stepName: stepName) + fulfill(submission) + }.catch { + error in + reject(error) } + } + } +} - if response?.statusCode == 201 { - let submission = Submission(json: json["submissions"].arrayValue[0], stepName: stepName) - success(submission) - return - } else { - errorHandler("Response status code is wrong(\(String(describing: response?.statusCode)))") - return - } - }) +extension SubmissionsAPI { + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func create(stepName: String, attemptId: Int, reply: Reply, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping (Submission) -> Void, error errorHandler: @escaping (String) -> Void) -> Request? { + self.create(stepName: stepName, attemptId: attemptId, reply: reply).then { + submission in + success(submission) + }.catch { + error in + errorHandler(error.localizedDescription) + } + return nil } } diff --git a/Stepic/Unit.swift b/Stepic/Unit.swift index 12a3fe5539..a16475cdd4 100644 --- a/Stepic/Unit.swift +++ b/Stepic/Unit.swift @@ -10,9 +10,9 @@ import Foundation import CoreData import SwiftyJSON -class Unit: NSManagedObject, JSONInitializable { +class Unit: NSManagedObject, JSONSerializable { - typealias idType = Int + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -38,10 +38,6 @@ class Unit: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } - func loadAssignments(_ completion: @escaping (() -> Void), errorHandler: @escaping (() -> Void)) { _ = ApiDataDownloader.assignments.retrieve(ids: self.assignmentsArray, existing: self.assignments, refreshMode: .update, success: { newAssignments in diff --git a/Stepic/UnitsAPI.swift b/Stepic/UnitsAPI.swift index 1a60b32c84..b2d19b4463 100644 --- a/Stepic/UnitsAPI.swift +++ b/Stepic/UnitsAPI.swift @@ -14,59 +14,53 @@ import PromiseKit class UnitsAPI: APIEndpoint { override var name: String { return "units" } - func retrieve(lesson lessonId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise { - return Promise { fulfill, reject in - retrieve(lesson: lessonId, headers: headers, success: { unit in - fulfill(unit) - }, error: { err in - reject(err) - }) - } - } - - @discardableResult func retrieve(lesson lessonId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ((Unit) -> Void), error errorHandler: @escaping ((UnitRetrieveError) -> Void)) -> Request { - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/\(name)?lesson=\(lessonId)", headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } - let response = response.response - - if let e = error as NSError? { - print("RETRIEVE units?\(lessonId): error \(e.domain) \(e.code): \(e.localizedDescription)") - errorHandler(.connectionError) - return - } - - if response?.statusCode != 200 { - print("RETRIEVE units?\(lessonId)): bad response status code \(String(describing: response?.statusCode))") - errorHandler(.badStatus) - return - } - - let units = json["units"].arrayValue.map({return Unit(json: $0)}) - - guard let unit = units.first else { - errorHandler(.noUnits) + //TODO: Seems like a bug. Fix this when fixing CoreData duplicates + func retrieve(lesson lessonId: Int) -> Promise { + let params: Parameters = ["lesson": lessonId] + return Promise { + fulfill, reject in + retrieve.request(requestEndpoint: "units", paramName: "units", params: params, updatingObjects: Array(), withManager: manager).then { + units, _, _ -> Void in + guard let unit: Unit = units.first else { + reject(UnitRetrieveError.noUnits) return } - - success(unit) - - return + fulfill(unit) + }.catch { + error in + reject(error) } - ) +// This is a correct replacement after CoreData duplicates fix for this +// retrieve.requestWithFetching(requestEndpoint: "units", paramName: "units", params: params, withManager: manager).then { +// (units, _) -> Void in +// guard let unit: Unit = units.first else { +// reject(UnitRetrieveError.noUnits) +// return +// } +// fulfill(unit) +// }.catch { +// error in +// reject(error) +// } + } } @discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [Unit], refreshMode: RefreshMode, success: @escaping (([Unit]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { - return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) + return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) + } +} + +extension UnitsAPI { + @available(*, deprecated, message: "Legacy method with callbacks") + @discardableResult func retrieve(lesson lessonId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ((Unit) -> Void), error errorHandler: @escaping ((Error) -> Void)) -> Request? { + retrieve(lesson: lessonId).then { + unit in + success(unit) + }.catch { + error in + errorHandler(error) + } + return nil } } diff --git a/Stepic/UpdateRequestMaker.swift b/Stepic/UpdateRequestMaker.swift new file mode 100644 index 0000000000..efb59fcf6c --- /dev/null +++ b/Stepic/UpdateRequestMaker.swift @@ -0,0 +1,36 @@ +// +// UpdateRequestMaker.swift +// Stepic +// +// Created by Ostrenkiy on 20.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import Foundation +import Alamofire +import PromiseKit + +class UpdateRequestMaker { + func request(requestEndpoint: String, paramName: String, updatingObject: T, withManager manager: Alamofire.SessionManager) -> Promise { + return Promise { fulfill, reject in + let params: Parameters? = [ + paramName: updatingObject.json.dictionaryObject ?? "" + ] + + checkToken().then { + manager.request("\(StepicApplicationsInfo.apiURL)/\(requestEndpoint)/\(updatingObject.id)", method: .put, parameters: params, encoding: JSONEncoding.default).validate().responseSwiftyJSON { response in + switch response.result { + case .failure(let error): + reject(error) + case .success(let json): + updatingObject.update(json: json[requestEndpoint].arrayValue[0]) + fulfill(updatingObject) + } + } + }.catch { + error in + reject(error) + } + } + } +} diff --git a/Stepic/User.swift b/Stepic/User.swift index 32887cf90a..f86e733211 100644 --- a/Stepic/User.swift +++ b/Stepic/User.swift @@ -11,9 +11,9 @@ import CoreData import SwiftyJSON @objc -class User: NSManagedObject, JSONInitializable { +class User: NSManagedObject, JSONSerializable { - typealias idType = Int + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -36,10 +36,6 @@ class User: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } - var isGuest: Bool { return level == 0 } @@ -71,3 +67,16 @@ class User: NSManagedObject, JSONInitializable { } } } + +struct UserInfo { + var id: Int + var avatarURL: String + var firstName: String + var lastName: String + init(json: JSON) { + id = json["id"].intValue + avatarURL = json["avatar"].stringValue + firstName = json["first_name"].stringValue + lastName = json["last_name"].stringValue + } +} diff --git a/Stepic/UserActivitiesAPI.swift b/Stepic/UserActivitiesAPI.swift index 2f40120018..92b971645a 100644 --- a/Stepic/UserActivitiesAPI.swift +++ b/Stepic/UserActivitiesAPI.swift @@ -11,75 +11,27 @@ import Alamofire import SwiftyJSON import PromiseKit -class UserActivitiesAPI { - let name = "user-activities" +class UserActivitiesAPI: APIEndpoint { + override var name: String { return "user-activities" } - func retrieve(user userId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise { - return Promise { - fulfill, reject in - Alamofire.request("\(StepicApplicationsInfo.apiURL)/\(name)/\(userId)", headers: headers).responseSwiftyJSON { - response in - - switch response.result { - case .failure(let error): - reject(RetrieveError(error: error)) - return - case .success(let json): - guard let userActivityJSON = json["user-activities"].arrayValue.first else { - reject(RetrieveError.parsingError) - return - } - let userActivity = UserActivity(json: userActivityJSON) - fulfill(userActivity) - } - } - } + func retrieve(user userId: Int) -> Promise { + return retrieve.request(requestEndpoint: "user-activities", paramName: "user-activities", id: userId, withManager: manager) } } //deprecations extension UserActivitiesAPI { @available(*, deprecated, message: "Use retrieve with promises instead") - @discardableResult func retrieve(user userId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ((UserActivity) -> Void), error errorHandler: @escaping ((UserRetrieveError) -> Void)) -> Request { - return Alamofire.request("\(StepicApplicationsInfo.apiURL)/\(name)/\(userId)", headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } - let response = response.response - - if let e = error as NSError? { - print("RETRIEVE user-activities/\(userId): error \(e.domain) \(e.code): \(e.localizedDescription)") - errorHandler(.connectionError) - return - } - - if response?.statusCode != 200 { - print("RETRIEVE user-activities/\(userId): bad response status code \(String(describing: response?.statusCode))") - errorHandler(.badStatus) - return - } - - guard let userActivityJSON = json["user-activities"].arrayValue.first else { return } - let userActivity = UserActivity(json: userActivityJSON) + @discardableResult func retrieve(user userId: Int, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ((UserActivity) -> Void), error errorHandler: @escaping ((Error) -> Void)) -> Request? { + retrieve(user: userId).then { + userActivity in success(userActivity) + }.catch { + error in + errorHandler(error) + } - return - } - ) + return nil } } - -//TODO: Add parameters -@available(*, deprecated, message: "Use RetrieveError instead") -enum UserRetrieveError: Error { - case connectionError, badStatus -} diff --git a/Stepic/UserActivity.swift b/Stepic/UserActivity.swift index adb5cb47e8..48a4a118aa 100644 --- a/Stepic/UserActivity.swift +++ b/Stepic/UserActivity.swift @@ -9,7 +9,8 @@ import Foundation import SwiftyJSON -class UserActivity { +class UserActivity: JSONSerializable { + var id: Int var pins: [Int] @@ -18,7 +19,12 @@ class UserActivity { self.pins = UserActivity.emptyYearPins } - init(json: JSON) { + func update(json: JSON) { + self.id = json["id"].intValue + self.pins = json["pins"].arrayValue.map({return $0.intValue}) + } + + required init(json: JSON) { self.id = json["id"].intValue self.pins = json["pins"].arrayValue.map({return $0.intValue}) } diff --git a/Stepic/UsersAPI.swift b/Stepic/UsersAPI.swift index 116cf442af..81c338d1d7 100644 --- a/Stepic/UsersAPI.swift +++ b/Stepic/UsersAPI.swift @@ -22,6 +22,6 @@ class UsersAPI: APIEndpoint { extension UsersAPI { @available(*, deprecated, message: "Legacy method with callbacks") @discardableResult func retrieve(ids: [Int], headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, existing: [User], refreshMode: RefreshMode, success: @escaping (([User]) -> Void), error errorHandler: @escaping ((RetrieveError) -> Void)) -> Request? { - return getObjectsByIds(requestString: name, headers: headers, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) + return getObjectsByIds(requestString: name, printOutput: false, ids: ids, deleteObjects: existing, refreshMode: refreshMode, success: success, failure: errorHandler) } } diff --git a/Stepic/Video.swift b/Stepic/Video.swift index 277099922b..ae6a73a34e 100644 --- a/Stepic/Video.swift +++ b/Stepic/Video.swift @@ -16,9 +16,10 @@ enum VideoState { case online, downloading, cached } -class Video: NSManagedObject, JSONInitializable { +@objc +class Video: NSManagedObject, JSONSerializable { - typealias idType = Int + typealias IdType = Int convenience required init(json: JSON) { self.init() @@ -41,10 +42,6 @@ class Video: NSManagedObject, JSONInitializable { initialize(json) } - func hasEqualId(json: JSON) -> Bool { - return id == json["id"].intValue - } - static func getNearestDefault(to quality: String) -> String { let qualities = ["270", "360", "720", "1080"] var minDifference = 10000 diff --git a/Stepic/ViewsAPI.swift b/Stepic/ViewsAPI.swift index 49ee0d7c9e..392826bba9 100644 --- a/Stepic/ViewsAPI.swift +++ b/Stepic/ViewsAPI.swift @@ -14,6 +14,10 @@ import PromiseKit class ViewsAPI: APIEndpoint { override var name: String { return "views" } + func create(view: StepikModelView) -> Promise { + return create.request(requestEndpoint: "views", paramName: "view", creatingObject: view, withManager: manager) + } + func create(step stepId: Int, assignment assignmentId: Int?, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders) -> Promise { return Promise { fulfill, reject in create(stepId: stepId, assignment: assignmentId, headers: headers, success: { @@ -24,6 +28,7 @@ class ViewsAPI: APIEndpoint { } } + //TODO: Do not delete this until ViewsCreateError is handled correctly @discardableResult func create(stepId id: Int, assignment: Int?, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping () -> Void, error errorHandler: @escaping (ViewsCreateError) -> Void) -> Request? { var params: Parameters = [:] diff --git a/Stepic/Vote.swift b/Stepic/Vote.swift index 72b396ee3c..bd5349b8f5 100644 --- a/Stepic/Vote.swift +++ b/Stepic/Vote.swift @@ -9,28 +9,36 @@ import Foundation import SwiftyJSON -class Vote { - var id: String - var value: VoteValue? - - init(json: JSON) { +class Vote: JSONSerializable { + func update(json: JSON) { id = json["id"].stringValue if let v = json["value"].string { value = VoteValue(rawValue: v) } } + var id: String + var value: VoteValue? + + private init() { + id = "" + } + + convenience required init(json: JSON) { + self.init() + update(json: json) + } + init(id: String, value: VoteValue?) { self.id = id self.value = value } - var json: [String: AnyObject] { - let dict: [String: AnyObject] = [ - "id": id as AnyObject, - "value": value?.rawValue as AnyObject? ?? NSNull() + var json: JSON { + return [ + "id": id, + "value": value?.rawValue ?? NSNull() ] - return dict } } diff --git a/Stepic/VotesAPI.swift b/Stepic/VotesAPI.swift index 7e0d261efa..ed1ab18375 100644 --- a/Stepic/VotesAPI.swift +++ b/Stepic/VotesAPI.swift @@ -9,42 +9,20 @@ import Foundation import Alamofire import SwiftyJSON +import PromiseKit -class VotesAPI { +class VotesAPI: APIEndpoint { + override var name: String { return "votes" } - func update(_ vote: Vote, headers: [String: String] = AuthInfo.shared.initialHTTPHeaders, success: @escaping ((Vote) -> Void), error errorHandler: @escaping ((String) -> Void)) { - let params: Parameters? = [ - "vote": vote.json as AnyObject - ] - Alamofire.request("\(StepicApplicationsInfo.apiURL)/votes/\(vote.id)", method: .put, parameters: params, encoding: JSONEncoding.default, headers: headers).responseSwiftyJSON({ - response in - - var error = response.result.error - var json: JSON = [:] - if response.result.value == nil { - if error == nil { - error = NSError() - } - } else { - json = response.result.value! - } - let response = response.response - - if let e = error as NSError? { - errorHandler("PUT vote: error \(e.domain) \(e.code): \(e.localizedDescription)") - return - } - - if response?.statusCode != 200 { - errorHandler("PUT vote: bad response status code \(String(describing: response?.statusCode))") - return - } + func update(_ vote: Vote) -> Promise { + return update.request(requestEndpoint: "votes", paramName: "vote", updatingObject: vote, withManager: manager) + } - let retrievedVote = Vote(json: json["votes"].arrayValue[0]) - success(retrievedVote) +} - return - } - ) +extension VotesAPI { + @available(*, deprecated, message: "Legacy method with callbacks") + func update(_ vote: Vote, success: @escaping ((Vote) -> Void), error errorHandler: @escaping ((String) -> Void)) { + update(vote).then { success($0) }.catch { errorHandler($0.localizedDescription) } } } diff --git a/Stepic/WriteCommentViewController.swift b/Stepic/WriteCommentViewController.swift index d47b18ef22..72cc6fc7e1 100644 --- a/Stepic/WriteCommentViewController.swift +++ b/Stepic/WriteCommentViewController.swift @@ -97,7 +97,7 @@ class WriteCommentViewController: UIViewController { } func sendComment() { - let comment = CommentPostable(parent: parentId, target: target, text: htmlText) + let comment = Comment(parent: parentId, target: target, text: htmlText) request = ApiDataDownloader.comments.create(comment, success: { [weak self] diff --git a/Stepic/ru.lproj/Localizable.strings b/Stepic/ru.lproj/Localizable.strings index 8e61647fa6..cd4cde6386 100644 --- a/Stepic/ru.lproj/Localizable.strings +++ b/Stepic/ru.lproj/Localizable.strings @@ -440,7 +440,6 @@ ArtCustomizeLearningProcess = "art_customize_learning_process_ru"; "Please, log in to see the user courses catalog" = "Пожалуйста, войдите, чтобы увидеть вышии курсы"; "Answer" = "Ответ"; "This quiz is unavailable on AppleTV" = "Это задание недоступно на AppleTV"; -"Submit" = "Подтвердить"; "Try Again" = "Попробовать снова"; "Correct" = "Верно"; "Wrong" = "Неверно"; diff --git a/StepicTests/CertificatesAPITest.swift b/StepicTests/CertificatesAPITest.swift deleted file mode 100644 index 5e39e82662..0000000000 --- a/StepicTests/CertificatesAPITest.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// CertificatesAPITest.swift -// Stepic -// -// Created by Ostrenkiy on 12.04.17. -// Copyright © 2017 Alex Karpov. All rights reserved. -// - -import Foundation -import XCTest -import UIKit -@testable import Stepic - -class CertificatesAPITests: XCTestCase { - - let certificates = CertificatesAPI() - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() - } - - func testGetCertificates() { - let expectation = self.expectation(description: "testGetCertificates") - - certificates.retrieve(userId: 1718803, page: 1, success: { - _, certificates in - print(certificates) - expectation.fulfill() - }, error: { - error in - XCTAssert(false, "error \(error)") - }) - - waitForExpectations(timeout: 10.0) { - error in - if error != nil { - XCTAssert(false, "Timeout error") - } - } - } - - func testUpdateCertificates() { - let expectation = self.expectation(description: "testUpdateCertificates") - - let cert = Certificate() - cert.id = 8715 - cert.grade = 50 - - certificates.retrieve(userId: 1718803, page: 1, success: { - _, certificates in - print(certificates) - expectation.fulfill() - }, error: { - error in - XCTAssert(false, "error \(error)") - }) - - waitForExpectations(timeout: 10.0) { - error in - if error != nil { - XCTAssert(false, "Timeout error") - } - } - } -} diff --git a/StepicTests/CookieTests.swift b/StepicTests/CookieTests.swift index 5c1d92e960..08d44bf33f 100644 --- a/StepicTests/CookieTests.swift +++ b/StepicTests/CookieTests.swift @@ -33,9 +33,7 @@ class CookieTests: XCTestCase { print("retrieved user \(user.id) \(user.firstName) \(user.lastName)") ApiDataDownloader.attempts.create(stepName: "choice", stepId: 115260, success: { attempt in - if let id = attempt.id { - print("created attempt \(id)") - } + print("created attempt \(attempt.id)") expectation.fulfill() }, error: { errorMsg in From cd18774b4752c9d2eb9f904edd95eabc3a98c1f6 Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Wed, 4 Apr 2018 11:25:28 +0300 Subject: [PATCH 07/14] Fix crash in discussions (#264) * Make step field in DiscussionsViewController optional * Update comment view in VideoStepViewController * Edit comment --- Stepic/DiscussionsViewController.swift | 6 ++++-- Stepic/VideoStepViewController.swift | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Stepic/DiscussionsViewController.swift b/Stepic/DiscussionsViewController.swift index 3a46b2794d..9f7ccd4e1b 100644 --- a/Stepic/DiscussionsViewController.swift +++ b/Stepic/DiscussionsViewController.swift @@ -42,7 +42,9 @@ class DiscussionsViewController: UIViewController { var discussionProxyId: String! var target: Int! - var step: Step! + + // This var is used only for incrementing discussions count + var step: Step? @IBOutlet weak var tableView: UITableView! @@ -725,7 +727,7 @@ extension DiscussionsViewController : WriteCommentDelegate { discussionIds.loaded.insert(comment.id, at: 0) discussions.insert(comment, at: 0) reloadTableData() - step.discussionsCount? += 1 + step?.discussionsCount? += 1 } } } diff --git a/Stepic/VideoStepViewController.swift b/Stepic/VideoStepViewController.swift index ab8cea1dbc..6f5812e6ff 100644 --- a/Stepic/VideoStepViewController.swift +++ b/Stepic/VideoStepViewController.swift @@ -165,6 +165,10 @@ class VideoStepViewController: UIViewController { let downloadItem = UIBarButtonItem(customView: itemView) let shareBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.action, target: self, action: #selector(VideoStepViewController.sharePressed(_:))) nItem.rightBarButtonItems = [shareBarButtonItem, downloadItem] + + if let discussionCount = step.discussionsCount { + discussionCountView.commentsCount = discussionCount + } } override func didReceiveMemoryWarning() { @@ -253,6 +257,7 @@ class VideoStepViewController: UIViewController { let vc = DiscussionsViewController(nibName: "DiscussionsViewController", bundle: nil) vc.discussionProxyId = discussionProxyId vc.target = self.step.id + vc.step = self.step nController?.pushViewController(vc, animated: true) } else { //TODO: Load comments here From 206a7693d43e9fac14a5c5127e67e3bafd4e0f4d Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Wed, 4 Apr 2018 11:25:53 +0300 Subject: [PATCH 08/14] Replace old placeholders & empty states (#256) * New placeholder view * Add UITableView subclass with placeholders support * Add more placeholders * Add L10n * Some fixes * Vector assets in PDF * Use updated empty state in notifications * Use updated empty state in downloads * Hide image container when image is nil * Add hideable button * Add loading state to StepikTableView * Add StepikViewController & anonymous state for certificates * Remove old placeholders & empty states: certificates * Add retry action for certificates placeholder * Remove old placeholders & empty states: discussions * Remove old placeholders & empty states: profile * Remove old placeholders & empty states: sections * Remove old placeholders & empty states: units * Remove DZNEmptyDataSet from dependencies * Remove old placeholders & empty states: quiz * Subclass -> protocol * Remove old placeholders & empty states: notifications * Rename file & remove unused code * Remove old placeholders & empty states: card steps * Remove old placeholders & empty states: course select (adaptive) * Remove PlaceholderView * Fix placeholder visibility * Fix placeholder updates * Remove internal modificator --- Podfile | 1 - Stepic.xcodeproj/project.pbxproj | 182 +++---------- Stepic/Base.lproj/Main.storyboard | 6 +- Stepic/CardsStepsViewController.swift | 91 ++----- Stepic/CertificatesStoryboard.storyboard | 2 +- Stepic/CertificatesViewController.swift | 160 ++---------- Stepic/ControllerWithStepikPlaceholder.swift | 106 ++++++++ Stepic/DiscussionsViewController.swift | 97 ++----- Stepic/DiscussionsViewController.xib | 11 +- Stepic/DownloadsViewController.swift | 2 - Stepic/MenuViewController.swift | 2 +- Stepic/NotificationsPagerViewController.swift | 62 ++--- Stepic/NotificationsViewController.swift | 1 - Stepic/PlaceholderStyleExtensions.swift | 51 ---- Stepic/PlaceholderTestViewController.swift | 60 ----- Stepic/PlaceholderView.storyboard | 26 -- Stepic/PlaceholderView.swift | 241 ------------------ Stepic/PlaceholderViewDataSource.swift | 17 -- Stepic/PlaceholderViewDelegate.swift | 14 - Stepic/ProfileViewController.swift | 148 +++-------- Stepic/QuizViewController.swift | 64 +---- Stepic/SectionsViewController.swift | 99 ++----- Stepic/Stepic-Bridging-Header.h | 2 - Stepic/StepikPlaceholder.swift | 48 +--- .../StepikPlaceholderStyle+Placeholders.swift | 90 +++++++ Stepic/StepikPlaceholderView.swift | 30 +-- Stepic/StepikTableView.swift | 65 +++-- Stepic/Styles.swift | 44 ---- Stepic/UnitsViewController.swift | 99 ++----- .../AdaptiveCardsStepsViewController.swift | 9 + .../AdaptiveCourseSelectViewController.swift | 82 ++---- 31 files changed, 475 insertions(+), 1437 deletions(-) create mode 100644 Stepic/ControllerWithStepikPlaceholder.swift delete mode 100644 Stepic/PlaceholderStyleExtensions.swift delete mode 100644 Stepic/PlaceholderTestViewController.swift delete mode 100644 Stepic/PlaceholderView.storyboard delete mode 100644 Stepic/PlaceholderView.swift delete mode 100644 Stepic/PlaceholderViewDataSource.swift delete mode 100644 Stepic/PlaceholderViewDelegate.swift create mode 100644 Stepic/StepikPlaceholderStyle+Placeholders.swift delete mode 100644 Stepic/Styles.swift diff --git a/Podfile b/Podfile index 7967fa3672..e06b6b4de4 100644 --- a/Podfile +++ b/Podfile @@ -24,7 +24,6 @@ def all_pods pod 'SVProgressHUD' pod 'FLKAutoLayout', '1.0.1' pod 'TSMessages', :git => 'https://github.com/KrauseFx/TSMessages.git' - pod 'DZNEmptyDataSet' pod 'YandexMobileMetrica/Dynamic' pod 'FirebaseCore' diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index b00287aba4..9e6c8741cb 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -761,13 +761,6 @@ 0897009B1F6B2A830041C24E /* NibInitializableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089700951F6B2A820041C24E /* NibInitializableView.swift */; }; 0897009C1F6B2A830041C24E /* NibInitializableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089700951F6B2A820041C24E /* NibInitializableView.swift */; }; 0897009D1F6B2A830041C24E /* NibInitializableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089700951F6B2A820041C24E /* NibInitializableView.swift */; }; - 08970ECF1C6A326900846119 /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECA1C6A326900846119 /* PlaceholderView.swift */; }; - 08970ED01C6A326900846119 /* PlaceholderViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */; }; - 08970ED11C6A326900846119 /* PlaceholderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */; }; - 08970ED21C6A326900846119 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECD1C6A326900846119 /* Styles.swift */; }; - 08970ED31C6A326900846119 /* PlaceholderStyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */; }; - 08970ED71C6A36E500846119 /* PlaceholderTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */; }; - 08970ED81C6A36E500846119 /* PlaceholderView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */; }; 089984291ECDE188005C0B27 /* LessonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089984281ECDE188005C0B27 /* LessonViewController.swift */; }; 0899842A1ECDE188005C0B27 /* LessonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089984281ECDE188005C0B27 /* LessonViewController.swift */; }; 0899842C1ECDE194005C0B27 /* LessonPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0899842B1ECDE194005C0B27 /* LessonPresenter.swift */; }; @@ -1103,9 +1096,7 @@ 08D1207A1C937B2200A54ABC /* Dataset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F5554F1C4F93B700C877E8 /* Dataset.swift */; }; 08D1207B1C937B2200A54ABC /* WebStepViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0891424F1BCFFB7F0000BCB0 /* WebStepViewController.swift */; }; 08D1207D1C937B2200A54ABC /* LabelExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CA59E91BBD3D55008DC44D /* LabelExtension.swift */; }; - 08D1207E1C937B2200A54ABC /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECA1C6A326900846119 /* PlaceholderView.swift */; }; 08D1207F1C937B2200A54ABC /* Progress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083AABE71BE8D63C005E1E96 /* Progress.swift */; }; - 08D120801C937B2200A54ABC /* PlaceholderStyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */; }; 08D120811C937B2200A54ABC /* MathQuizViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F485AB1C580DB3000165AA /* MathQuizViewController.swift */; }; 08D120821C937B2200A54ABC /* VideoURL+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089142461BCEE4EE0000BCB0 /* VideoURL+CoreDataProperties.swift */; }; 08D120831C937B2200A54ABC /* Video+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089142441BCEE4EE0000BCB0 /* Video+CoreDataProperties.swift */; }; @@ -1130,12 +1121,10 @@ 08D120981C937B2200A54ABC /* PathManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DF1D8B1BDA77A200BA35EA /* PathManager.swift */; }; 08D120991C937B2200A54ABC /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DF1D911BDAB93900BA35EA /* StringExtensions.swift */; }; 08D1209A1C937B2200A54ABC /* Attempt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F5554D1C4F924000C877E8 /* Attempt.swift */; }; - 08D1209B1C937B2200A54ABC /* PlaceholderViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */; }; 08D1209C1C937B2200A54ABC /* ControllerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083D64AE1C19BDB2003222F0 /* ControllerHelper.swift */; }; 08D1209D1C937B2200A54ABC /* Progress+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083AABE61BE8D63C005E1E96 /* Progress+CoreDataProperties.swift */; }; 08D1209E1C937B2200A54ABC /* ButtonExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CA59ED1BBFC962008DC44D /* ButtonExtension.swift */; }; 08D120A01C937B2200A54ABC /* TeachersTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CA59E51BBD25DC008DC44D /* TeachersTableViewCell.swift */; }; - 08D120A11C937B2200A54ABC /* PlaceholderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */; }; 08D120A21C937B2200A54ABC /* SortingQuizTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F485B61C58ECE3000165AA /* SortingQuizTableViewCell.swift */; }; 08D120A31C937B2200A54ABC /* UIViewExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 089A0DA61BE9FFCE004AF4EB /* UIViewExtensions.swift */; }; 08D120A41C937B2200A54ABC /* Section+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF6C1BC5D177000AFEA7 /* Section+CoreDataProperties.swift */; }; @@ -1172,7 +1161,6 @@ 08D120C91C937B2200A54ABC /* ChoiceReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F5555B1C4FB1E300C877E8 /* ChoiceReply.swift */; }; 08D120CA1C937B2200A54ABC /* VideoQualityTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F74651BD924B80064AAEA /* VideoQualityTableViewController.swift */; }; 08D120CB1C937B2200A54ABC /* MathReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F485A91C580D61000165AA /* MathReply.swift */; }; - 08D120CC1C937B2200A54ABC /* PlaceholderTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */; }; 08D120CD1C937B2200A54ABC /* StepicVideoPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08EF9A051C91D0F800433E4A /* StepicVideoPlayerViewController.swift */; }; 08D120CE1C937B2200A54ABC /* StringDatasetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F555551C4F9FAB00C877E8 /* StringDatasetExtension.swift */; }; 08D120CF1C937B2200A54ABC /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08CA59F01BBFD65E008DC44D /* User.swift */; }; @@ -1206,7 +1194,6 @@ 08D120F01C937B2200A54ABC /* UIColorExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DE94381B8E3FCE00D278AB /* UIColorExtensions.swift */; }; 08D120F11C937B2200A54ABC /* Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0885F8531BA9DB5C00F2A188 /* Meta.swift */; }; 08D120F31C937B2200A54ABC /* StepicToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080F31DC1BA7162C00F356A0 /* StepicToken.swift */; }; - 08D120F41C937B2200A54ABC /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECD1C6A326900846119 /* Styles.swift */; }; 08D120F51C937B2200A54ABC /* SectionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0828FF781BC5E744000AFEA7 /* SectionsViewController.swift */; }; 08D120F61C937B2200A54ABC /* Sorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A1256E1BDE8E460066B2B2 /* Sorter.swift */; }; 08D120F81C937B2200A54ABC /* ChoiceDataset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F555531C4F97C100C877E8 /* ChoiceDataset.swift */; }; @@ -1232,7 +1219,6 @@ 08D121161C937B2200A54ABC /* Scripts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 08DF1D951BDADB7A00BA35EA /* Scripts.plist */; }; 08D121171C937B2200A54ABC /* VideoDownloadView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 086E965D1BF6683800AB952D /* VideoDownloadView.xib */; }; 08D121181C937B2200A54ABC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 08A1257B1BDEBCC90066B2B2 /* Localizable.strings */; }; - 08D121191C937B2200A54ABC /* PlaceholderView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */; }; 08D1211A1C937B2200A54ABC /* WarningView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08F485941C574DAD000165AA /* WarningView.xib */; }; 08D1211B1C937B2200A54ABC /* TeacherCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08CA59DB1BBC00B9008DC44D /* TeacherCollectionViewCell.xib */; }; 08D1211C1C937B2200A54ABC /* SectionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0828FF751BC5E6B0000AFEA7 /* SectionTableViewCell.xib */; }; @@ -1620,13 +1606,7 @@ 2C1B60EB1F4C4AEF00236804 /* RateAppAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082799471E9B81AF008A3786 /* RateAppAlertManager.swift */; }; 2C1B60EC1F4C4AEF00236804 /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 08E16F221BDBA4AF004822E1 /* Reachability.m */; }; 2C1B60ED1F4C4AEF00236804 /* ControllerHelperLaunchExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DA79001DB6BB36003491C4 /* ControllerHelperLaunchExtension.swift */; }; - 2C1B60EE1F4C4AEF00236804 /* PlaceholderTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */; }; - 2C1B60EF1F4C4AEF00236804 /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECA1C6A326900846119 /* PlaceholderView.swift */; }; 2C1B60F01F4C4AEF00236804 /* CodeSnippetSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0860D9111F10C5480087D61B /* CodeSnippetSymbols.swift */; }; - 2C1B60F11F4C4AEF00236804 /* PlaceholderViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */; }; - 2C1B60F21F4C4AEF00236804 /* PlaceholderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */; }; - 2C1B60F31F4C4AEF00236804 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECD1C6A326900846119 /* Styles.swift */; }; - 2C1B60F41F4C4AEF00236804 /* PlaceholderStyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */; }; 2C1B60F51F4C4AEF00236804 /* VideoDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086E965F1BF66A1800AB952D /* VideoDownloadView.swift */; }; 2C1B60F61F4C4AEF00236804 /* CodeSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C5E7A1EFC13ED0036EB3D /* CodeSample.swift */; }; 2C1B60F71F4C4AEF00236804 /* AdaptiveRatingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C76ACCC1F16496C0077D9D7 /* AdaptiveRatingManager.swift */; }; @@ -1933,7 +1913,6 @@ 2C1B625F1F4C4AEF00236804 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08DE941A1B8C58AC00D278AB /* Main.storyboard */; }; 2C1B62601F4C4AEF00236804 /* StepCardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 860DB7911EB8DC13001E3E42 /* StepCardView.xib */; }; 2C1B62611F4C4AEF00236804 /* CodeSuggestionsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0860D9181F115D830087D61B /* CodeSuggestionsTableViewController.xib */; }; - 2C1B62621F4C4AEF00236804 /* PlaceholderView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */; }; 2C1B62631F4C4AEF00236804 /* VideoDownloadView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 086E965D1BF6683800AB952D /* VideoDownloadView.xib */; }; 2C1B62651F4C4AEF00236804 /* WarningView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08F485941C574DAD000165AA /* WarningView.xib */; }; 2C1B62671F4C4AEF00236804 /* StepTabView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 083622DB1CD1FA6700CD8915 /* StepTabView.xib */; }; @@ -2024,13 +2003,7 @@ 2C1B62FC1F4C590700236804 /* RateAppAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082799471E9B81AF008A3786 /* RateAppAlertManager.swift */; }; 2C1B62FD1F4C590700236804 /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 08E16F221BDBA4AF004822E1 /* Reachability.m */; }; 2C1B62FE1F4C590700236804 /* ControllerHelperLaunchExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DA79001DB6BB36003491C4 /* ControllerHelperLaunchExtension.swift */; }; - 2C1B62FF1F4C590700236804 /* PlaceholderTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */; }; - 2C1B63001F4C590700236804 /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECA1C6A326900846119 /* PlaceholderView.swift */; }; 2C1B63011F4C590700236804 /* CodeSnippetSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0860D9111F10C5480087D61B /* CodeSnippetSymbols.swift */; }; - 2C1B63021F4C590700236804 /* PlaceholderViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */; }; - 2C1B63031F4C590700236804 /* PlaceholderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */; }; - 2C1B63041F4C590700236804 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECD1C6A326900846119 /* Styles.swift */; }; - 2C1B63051F4C590700236804 /* PlaceholderStyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */; }; 2C1B63061F4C590700236804 /* VideoDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086E965F1BF66A1800AB952D /* VideoDownloadView.swift */; }; 2C1B63071F4C590700236804 /* CodeSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C5E7A1EFC13ED0036EB3D /* CodeSample.swift */; }; 2C1B63081F4C590700236804 /* AdaptiveRatingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C76ACCC1F16496C0077D9D7 /* AdaptiveRatingManager.swift */; }; @@ -2337,7 +2310,6 @@ 2C1B64701F4C590700236804 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08DE941A1B8C58AC00D278AB /* Main.storyboard */; }; 2C1B64711F4C590700236804 /* StepCardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 860DB7911EB8DC13001E3E42 /* StepCardView.xib */; }; 2C1B64721F4C590700236804 /* CodeSuggestionsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0860D9181F115D830087D61B /* CodeSuggestionsTableViewController.xib */; }; - 2C1B64731F4C590700236804 /* PlaceholderView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */; }; 2C1B64741F4C590700236804 /* VideoDownloadView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 086E965D1BF6683800AB952D /* VideoDownloadView.xib */; }; 2C1B64761F4C590700236804 /* WarningView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08F485941C574DAD000165AA /* WarningView.xib */; }; 2C1B64781F4C590700236804 /* StepTabView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 083622DB1CD1FA6700CD8915 /* StepTabView.xib */; }; @@ -2773,13 +2745,7 @@ 2C89A97F1F4C289900227C3B /* RateAppAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082799471E9B81AF008A3786 /* RateAppAlertManager.swift */; }; 2C89A9801F4C289900227C3B /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 08E16F221BDBA4AF004822E1 /* Reachability.m */; }; 2C89A9811F4C289900227C3B /* ControllerHelperLaunchExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DA79001DB6BB36003491C4 /* ControllerHelperLaunchExtension.swift */; }; - 2C89A9821F4C289900227C3B /* PlaceholderTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */; }; - 2C89A9831F4C289900227C3B /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECA1C6A326900846119 /* PlaceholderView.swift */; }; 2C89A9841F4C289900227C3B /* CodeSnippetSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0860D9111F10C5480087D61B /* CodeSnippetSymbols.swift */; }; - 2C89A9851F4C289900227C3B /* PlaceholderViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */; }; - 2C89A9861F4C289900227C3B /* PlaceholderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */; }; - 2C89A9871F4C289900227C3B /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECD1C6A326900846119 /* Styles.swift */; }; - 2C89A9881F4C289900227C3B /* PlaceholderStyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */; }; 2C89A9891F4C289900227C3B /* VideoDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086E965F1BF66A1800AB952D /* VideoDownloadView.swift */; }; 2C89A98A1F4C289900227C3B /* CodeSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C5E7A1EFC13ED0036EB3D /* CodeSample.swift */; }; 2C89A98B1F4C289900227C3B /* AdaptiveRatingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C76ACCC1F16496C0077D9D7 /* AdaptiveRatingManager.swift */; }; @@ -3085,7 +3051,6 @@ 2C89AAF01F4C289900227C3B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08DE941A1B8C58AC00D278AB /* Main.storyboard */; }; 2C89AAF11F4C289900227C3B /* StepCardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 860DB7911EB8DC13001E3E42 /* StepCardView.xib */; }; 2C89AAF21F4C289900227C3B /* CodeSuggestionsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0860D9181F115D830087D61B /* CodeSuggestionsTableViewController.xib */; }; - 2C89AAF31F4C289900227C3B /* PlaceholderView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */; }; 2C89AAF41F4C289900227C3B /* VideoDownloadView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 086E965D1BF6683800AB952D /* VideoDownloadView.xib */; }; 2C89AAF61F4C289900227C3B /* WarningView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08F485941C574DAD000165AA /* WarningView.xib */; }; 2C89AAF71F4C289900227C3B /* MathJax in Resources */ = {isa = PBXBuildFile; fileRef = 08E542001C6A76E100DEC38E /* MathJax */; }; @@ -3182,13 +3147,7 @@ 2C9731331F4C38F600AC9301 /* RateAppAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082799471E9B81AF008A3786 /* RateAppAlertManager.swift */; }; 2C9731341F4C38F600AC9301 /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 08E16F221BDBA4AF004822E1 /* Reachability.m */; }; 2C9731351F4C38F600AC9301 /* ControllerHelperLaunchExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DA79001DB6BB36003491C4 /* ControllerHelperLaunchExtension.swift */; }; - 2C9731361F4C38F600AC9301 /* PlaceholderTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */; }; - 2C9731371F4C38F600AC9301 /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECA1C6A326900846119 /* PlaceholderView.swift */; }; 2C9731381F4C38F600AC9301 /* CodeSnippetSymbols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0860D9111F10C5480087D61B /* CodeSnippetSymbols.swift */; }; - 2C9731391F4C38F600AC9301 /* PlaceholderViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */; }; - 2C97313A1F4C38F600AC9301 /* PlaceholderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */; }; - 2C97313B1F4C38F600AC9301 /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECD1C6A326900846119 /* Styles.swift */; }; - 2C97313C1F4C38F600AC9301 /* PlaceholderStyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */; }; 2C97313D1F4C38F600AC9301 /* VideoDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086E965F1BF66A1800AB952D /* VideoDownloadView.swift */; }; 2C97313E1F4C38F600AC9301 /* CodeSample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080C5E7A1EFC13ED0036EB3D /* CodeSample.swift */; }; 2C97313F1F4C38F600AC9301 /* AdaptiveRatingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C76ACCC1F16496C0077D9D7 /* AdaptiveRatingManager.swift */; }; @@ -3495,7 +3454,6 @@ 2C9732A41F4C38F600AC9301 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08DE941A1B8C58AC00D278AB /* Main.storyboard */; }; 2C9732A51F4C38F600AC9301 /* StepCardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 860DB7911EB8DC13001E3E42 /* StepCardView.xib */; }; 2C9732A61F4C38F600AC9301 /* CodeSuggestionsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 0860D9181F115D830087D61B /* CodeSuggestionsTableViewController.xib */; }; - 2C9732A71F4C38F600AC9301 /* PlaceholderView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */; }; 2C9732A81F4C38F600AC9301 /* VideoDownloadView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 086E965D1BF6683800AB952D /* VideoDownloadView.xib */; }; 2C9732AA1F4C38F600AC9301 /* WarningView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08F485941C574DAD000165AA /* WarningView.xib */; }; 2C9732AC1F4C38F600AC9301 /* StepTabView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 083622DB1CD1FA6700CD8915 /* StepTabView.xib */; }; @@ -3895,12 +3853,6 @@ 864D673E1E83DE8A001E8D9E /* TCBlobDownloadManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 080ACF5F1BD7DE2A00329F2B /* TCBlobDownloadManager.swift */; }; 864D673F1E83DE8A001E8D9E /* Alamofire-SwiftyJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0824289B1BB0104700C98185 /* Alamofire-SwiftyJSON.swift */; }; 864D67401E83DE8A001E8D9E /* JSQWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 083B164C1C2AF27700250B37 /* JSQWebViewController.swift */; }; - 864D67411E83E08E001E8D9E /* PlaceholderTestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */; }; - 864D67431E83E08E001E8D9E /* PlaceholderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECA1C6A326900846119 /* PlaceholderView.swift */; }; - 864D67441E83E08E001E8D9E /* PlaceholderViewDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */; }; - 864D67451E83E08E001E8D9E /* PlaceholderViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */; }; - 864D67461E83E08E001E8D9E /* Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECD1C6A326900846119 /* Styles.swift */; }; - 864D67471E83E08E001E8D9E /* PlaceholderStyleExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */; }; 864D67481E83E08E001E8D9E /* VideoDownloadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086E965F1BF66A1800AB952D /* VideoDownloadView.swift */; }; 864D674A1E83E08E001E8D9E /* WarningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F485921C574D79000165AA /* WarningView.swift */; }; 864D674C1E83E08E001E8D9E /* UIViewLoadExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08F485971C5752D0000165AA /* UIViewLoadExtension.swift */; }; @@ -3979,12 +3931,28 @@ 864D67D61E83E394001E8D9E /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = 08E16F221BDBA4AF004822E1 /* Reachability.m */; }; 86624A731FC76578008E7E6C /* NotificationStatusesAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86624A721FC76578008E7E6C /* NotificationStatusesAPI.swift */; }; 86624A751FC76682008E7E6C /* NotificationsStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86624A741FC76682008E7E6C /* NotificationsStatus.swift */; }; + 866AD0D4206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */; }; + 866AD0D62061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D52061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift */; }; 86795EF11E84AA1400A985C2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08DE941A1B8C58AC00D278AB /* Main.storyboard */; }; 86795EF71E85325000A985C2 /* RecommendationsAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86795EF61E85325000A985C2 /* RecommendationsAPI.swift */; }; 868BC6E01EA0263300E204EE /* StreaksView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08F4761D1E82C11C0018E82C /* StreaksView.xib */; }; 868BC6E11EA0263300E204EE /* RateAppViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 089056101E98021000B8FE6A /* RateAppViewController.xib */; }; 869927D61ECFA9D9007D65A5 /* ShareableController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 082348381ECA264000D2CB40 /* ShareableController.swift */; }; - 86AE946E1E84511A00F67691 /* PlaceholderView.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */; }; + 86A1C3262065157C00D0914C /* StepikPlaceholderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF0885C205BED9700FCB9C0 /* StepikPlaceholderView.xib */; }; + 86A1C3272065157D00D0914C /* StepikPlaceholderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF0885C205BED9700FCB9C0 /* StepikPlaceholderView.xib */; }; + 86A1C3292065157E00D0914C /* StepikPlaceholderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF0885C205BED9700FCB9C0 /* StepikPlaceholderView.xib */; }; + 86A1C32A2065157F00D0914C /* StepikPlaceholderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF0885C205BED9700FCB9C0 /* StepikPlaceholderView.xib */; }; + 86A1C32B2065158000D0914C /* StepikPlaceholderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2CF0885C205BED9700FCB9C0 /* StepikPlaceholderView.xib */; }; + 86A1C331206515B200D0914C /* ControllerWithStepikPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D52061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift */; }; + 86A1C332206515B200D0914C /* ControllerWithStepikPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D52061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift */; }; + 86A1C333206515B300D0914C /* ControllerWithStepikPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D52061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift */; }; + 86A1C334206515B300D0914C /* ControllerWithStepikPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D52061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift */; }; + 86A1C335206515B400D0914C /* ControllerWithStepikPlaceholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D52061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift */; }; + 86A1C3362065173700D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */; }; + 86A1C3372065173700D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */; }; + 86A1C3382065173800D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */; }; + 86A1C3392065173800D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */; }; + 86A1C33A2065173900D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */; }; 86AE946F1E84511A00F67691 /* VideoDownloadView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 086E965D1BF6683800AB952D /* VideoDownloadView.xib */; }; 86AE94701E84511A00F67691 /* WarningView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 08F485941C574DAD000165AA /* WarningView.xib */; }; 86AE94711E84511A00F67691 /* StepTabView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 083622DB1CD1FA6700CD8915 /* StepTabView.xib */; }; @@ -4688,13 +4656,6 @@ 089611031D52250500561AC1 /* DeepLinkRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = DeepLinkRouter.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 08964BCC1F3072BA00DBBCCE /* QueriesAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueriesAPI.swift; sourceTree = ""; }; 089700951F6B2A820041C24E /* NibInitializableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NibInitializableView.swift; sourceTree = ""; }; - 08970ECA1C6A326900846119 /* PlaceholderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderView.swift; sourceTree = ""; }; - 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderViewDataSource.swift; sourceTree = ""; }; - 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderViewDelegate.swift; sourceTree = ""; }; - 08970ECD1C6A326900846119 /* Styles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Styles.swift; sourceTree = ""; }; - 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderStyleExtensions.swift; sourceTree = ""; }; - 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlaceholderTestViewController.swift; sourceTree = ""; }; - 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = PlaceholderView.storyboard; sourceTree = ""; }; 089984281ECDE188005C0B27 /* LessonViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LessonViewController.swift; sourceTree = ""; }; 0899842B1ECDE194005C0B27 /* LessonPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LessonPresenter.swift; sourceTree = ""; }; 0899842E1ECDE19E005C0B27 /* LessonView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LessonView.swift; sourceTree = ""; }; @@ -5178,6 +5139,8 @@ 863262F81ECFC5AE007A20B3 /* loading_robot.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = loading_robot.gif; sourceTree = ""; }; 86624A721FC76578008E7E6C /* NotificationStatusesAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusesAPI.swift; sourceTree = ""; }; 86624A741FC76682008E7E6C /* NotificationsStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsStatus.swift; sourceTree = ""; }; + 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StepikPlaceholderStyle+Placeholders.swift"; sourceTree = ""; }; + 866AD0D52061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerWithStepikPlaceholder.swift; sourceTree = ""; }; 86795EF61E85325000A985C2 /* RecommendationsAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RecommendationsAPI.swift; path = Stepic/RecommendationsAPI.swift; sourceTree = SOURCE_ROOT; }; 86ABC66B1E96867A0012E8A6 /* AdaptiveStepCardView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdaptiveStepCardView.swift; sourceTree = ""; }; 86BB7C012019538000063538 /* CongratsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CongratsView.swift; sourceTree = ""; }; @@ -6263,7 +6226,6 @@ 2C453396204D46B70061342A /* PinsMap */, 2C9704221F3B1BFE00C36F0A /* TapProxyView.swift */, 08F476201E82C12B0018E82C /* StreaksView */, - 08970EC91C6A325600846119 /* PlaceholderView */, 08F485911C574CFC000165AA /* VideoDownloadView */, 08F485961C574DB3000165AA /* WarningView */, 08F485971C5752D0000165AA /* UIViewLoadExtension.swift */, @@ -6472,28 +6434,6 @@ name = Step; sourceTree = ""; }; - 08970EC91C6A325600846119 /* PlaceholderView */ = { - isa = PBXGroup; - children = ( - 08970ED41C6A369200846119 /* PlaceholderViewTesting */, - 08970ECA1C6A326900846119 /* PlaceholderView.swift */, - 08970ECB1C6A326900846119 /* PlaceholderViewDataSource.swift */, - 08970ECC1C6A326900846119 /* PlaceholderViewDelegate.swift */, - 08970ECD1C6A326900846119 /* Styles.swift */, - 08970ECE1C6A326900846119 /* PlaceholderStyleExtensions.swift */, - ); - name = PlaceholderView; - sourceTree = ""; - }; - 08970ED41C6A369200846119 /* PlaceholderViewTesting */ = { - isa = PBXGroup; - children = ( - 08970ED51C6A36E500846119 /* PlaceholderTestViewController.swift */, - 08970ED61C6A36E500846119 /* PlaceholderView.storyboard */, - ); - name = PlaceholderViewTesting; - sourceTree = ""; - }; 089F58871D22BD44000CD540 /* DiscussionCountView */ = { isa = PBXGroup; children = ( @@ -6878,6 +6818,7 @@ 08CA59D41BBA1628008DC44D /* Stepic-Bridging-Header.h */, 08DA79001DB6BB36003491C4 /* ControllerHelperLaunchExtension.swift */, 080F211A2034DA2500A1204C /* LocalProgressLastViewedUpdater.swift */, + 866AD0D52061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift */, ); path = Stepic; sourceTree = ""; @@ -8044,6 +7985,7 @@ children = ( 2CF08860205BEE2B00FCB9C0 /* StepikPlaceholder.swift */, 2CF08862205BEEB800FCB9C0 /* StepikPlaceholderView */, + 866AD0D3206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift */, ); name = Placeholders; sourceTree = ""; @@ -8991,7 +8933,6 @@ 08ED08E31FCCDC3A0053FB68 /* UserActivityHomeView.xib in Resources */, 08DF78A91F5DE66200AEEA85 /* ArtView.xib in Resources */, 08EB85E21D0F192A00E4F345 /* LoadMoreTableViewCell.xib in Resources */, - 08D121191C937B2200A54ABC /* PlaceholderView.storyboard in Resources */, 083622DD1CD1FA6700CD8915 /* StepTabView.xib in Resources */, 08CBA31E1F57562A00302154 /* SwitchMenuBlockTableViewCell.xib in Resources */, 08FEFC1F1F117257005CA0FB /* CodeSuggestionTableViewCell.xib in Resources */, @@ -9098,7 +9039,6 @@ 08CB0D3A1FB5FB28001A1E02 /* Explore.storyboard in Resources */, 2CC351861F6827BE004255B6 /* Auth.storyboard in Resources */, 083622DC1CD1FA6700CD8915 /* StepTabView.xib in Resources */, - 08970ED81C6A36E500846119 /* PlaceholderView.storyboard in Resources */, 08F485951C574DAD000165AA /* WarningView.xib in Resources */, 089056131E98021000B8FE6A /* RateAppViewController.xib in Resources */, 0869F6D81CE3684000F8A6DB /* default_sound.wav in Resources */, @@ -9197,7 +9137,6 @@ 2C1B625F1F4C4AEF00236804 /* Main.storyboard in Resources */, 2C1B62601F4C4AEF00236804 /* StepCardView.xib in Resources */, 2C1B62611F4C4AEF00236804 /* CodeSuggestionsTableViewController.xib in Resources */, - 2C1B62621F4C4AEF00236804 /* PlaceholderView.storyboard in Resources */, 2C1B62631F4C4AEF00236804 /* VideoDownloadView.xib in Resources */, 2C1B62651F4C4AEF00236804 /* WarningView.xib in Resources */, 2C608B602045729C006870BB /* step2.html in Resources */, @@ -9246,6 +9185,7 @@ 08C7CB7A1FA7ACB9001D8241 /* CourseListEmptyPlaceholder.xib in Resources */, 08CBA35A1F5A2D9400302154 /* Profile.storyboard in Resources */, 2C1B62841F4C4AEF00236804 /* SearchSuggestionTableViewCell.xib in Resources */, + 86A1C32A2065157F00D0914C /* StepikPlaceholderView.xib in Resources */, 08CBA3231F57562A00302154 /* SwitchMenuBlockTableViewCell.xib in Resources */, 08EDD6261F7C607A005203E4 /* CourseWidgetTableViewCell.xib in Resources */, 2C608AC6204568C5006870BB /* CongratulationViewController.xib in Resources */, @@ -9307,7 +9247,6 @@ 2C1B64701F4C590700236804 /* Main.storyboard in Resources */, 2C1B64711F4C590700236804 /* StepCardView.xib in Resources */, 2C1B64721F4C590700236804 /* CodeSuggestionsTableViewController.xib in Resources */, - 2C1B64731F4C590700236804 /* PlaceholderView.storyboard in Resources */, 2C1B64741F4C590700236804 /* VideoDownloadView.xib in Resources */, 2C1B64761F4C590700236804 /* WarningView.xib in Resources */, 2C608B5F2045729B006870BB /* step2.html in Resources */, @@ -9356,6 +9295,7 @@ 08C7CB7B1FA7ACB9001D8241 /* CourseListEmptyPlaceholder.xib in Resources */, 08CBA35B1F5A2D9400302154 /* Profile.storyboard in Resources */, 2C1B64961F4C590700236804 /* step4.html in Resources */, + 86A1C3292065157E00D0914C /* StepikPlaceholderView.xib in Resources */, 08CBA3241F57562A00302154 /* SwitchMenuBlockTableViewCell.xib in Resources */, 08EDD6271F7C607A005203E4 /* CourseWidgetTableViewCell.xib in Resources */, 2C608AC5204568C5006870BB /* CongratulationViewController.xib in Resources */, @@ -9425,7 +9365,6 @@ 2C89AAF11F4C289900227C3B /* StepCardView.xib in Resources */, 2C35C4E31F4DA487002F3BF4 /* nouns_f.plist in Resources */, 2C89AAF21F4C289900227C3B /* CodeSuggestionsTableViewController.xib in Resources */, - 2C89AAF31F4C289900227C3B /* PlaceholderView.storyboard in Resources */, 08CB0D481FB63F74001A1E02 /* ContentLanguagesView.xib in Resources */, 2C6A762B2045CE4E00C509A6 /* ProgressTableViewCell.xib in Resources */, 2C89AAF41F4C289900227C3B /* VideoDownloadView.xib in Resources */, @@ -9455,6 +9394,7 @@ 2C89AB0F1F4C289900227C3B /* LoadMoreTableViewCell.xib in Resources */, 2C6A76532045CF3B00C509A6 /* overlay_hard.png in Resources */, 2C89AB101F4C289900227C3B /* DiscussionsStoryboard.storyboard in Resources */, + 86A1C3272065157D00D0914C /* StepikPlaceholderView.xib in Resources */, 08C7CB781FA7ACB9001D8241 /* CourseListEmptyPlaceholder.xib in Resources */, 2C89AB111F4C289900227C3B /* QuizViewController.xib in Resources */, 2C89AB121F4C289900227C3B /* step4.html in Resources */, @@ -9529,7 +9469,6 @@ 2C9732A61F4C38F600AC9301 /* CodeSuggestionsTableViewController.xib in Resources */, 2C608B612045729D006870BB /* step2.html in Resources */, 2C35C4EA1F4DA48F002F3BF4 /* nouns_m.plist in Resources */, - 2C9732A71F4C38F600AC9301 /* PlaceholderView.storyboard in Resources */, 2C9732A81F4C38F600AC9301 /* VideoDownloadView.xib in Resources */, 2C9732AA1F4C38F600AC9301 /* WarningView.xib in Resources */, 2C4BBF1F203DC66F000A4250 /* plyr.css in Resources */, @@ -9562,6 +9501,7 @@ 08195A261FA0AF2A00E6D6CD /* HorizontalCoursesView.xib in Resources */, 2C9732C21F4C38F600AC9301 /* DiscussionsViewController.xib in Resources */, 087E8B27204DE2A100C35B66 /* NotificationRequestAlertViewController.xib in Resources */, + 86A1C32B2065158000D0914C /* StepikPlaceholderView.xib in Resources */, 2C9732C31F4C38F600AC9301 /* LoadMoreTableViewCell.xib in Resources */, 08CBA3461F57565700302154 /* TitleContentExpandableMenuBlockTableViewCell.xib in Resources */, 2C9732C41F4C38F600AC9301 /* DiscussionsStoryboard.storyboard in Resources */, @@ -9674,7 +9614,6 @@ 2C8652601F4B2F7D00D51654 /* Config.plist in Resources */, 86795EF11E84AA1400A985C2 /* Main.storyboard in Resources */, 085E9E711F138C3F00D6A4BC /* CodeSuggestionsTableViewController.xib in Resources */, - 86AE946E1E84511A00F67691 /* PlaceholderView.storyboard in Resources */, 86AE946F1E84511A00F67691 /* VideoDownloadView.xib in Resources */, 86AE94701E84511A00F67691 /* WarningView.xib in Resources */, 86AE94711E84511A00F67691 /* StepTabView.xib in Resources */, @@ -9711,6 +9650,7 @@ 2C4BBF17203DC66D000A4250 /* plyr.js in Resources */, 2C8652641F4B2F7D00D51654 /* GoogleService-Info.plist in Resources */, 08195A231FA0AF2A00E6D6CD /* HorizontalCoursesView.xib in Resources */, + 86A1C3262065157C00D0914C /* StepikPlaceholderView.xib in Resources */, 86AE947C1E84511A00F67691 /* LoadMoreTableViewCell.xib in Resources */, 86AE947D1E84511A00F67691 /* DiscussionsStoryboard.storyboard in Resources */, 08CBA3431F57565700302154 /* TitleContentExpandableMenuBlockTableViewCell.xib in Resources */, @@ -9861,7 +9801,6 @@ "${BUILT_PRODUCTS_DIR}/CRToast/CRToast.framework", "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack-iOS/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework", "${BUILT_PRODUCTS_DIR}/DeviceKit-iOS/DeviceKit.framework", "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework", "${BUILT_PRODUCTS_DIR}/EasyTipView/EasyTipView.framework", @@ -9906,7 +9845,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CRToast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DeviceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EasyTipView.framework", @@ -10089,7 +10027,6 @@ "${BUILT_PRODUCTS_DIR}/CRToast/CRToast.framework", "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack-iOS/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework", "${BUILT_PRODUCTS_DIR}/DeviceKit-iOS/DeviceKit.framework", "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework", "${BUILT_PRODUCTS_DIR}/EasyTipView/EasyTipView.framework", @@ -10134,7 +10071,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CRToast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DeviceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EasyTipView.framework", @@ -10346,7 +10282,6 @@ "${BUILT_PRODUCTS_DIR}/CRToast/CRToast.framework", "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack-iOS/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework", "${BUILT_PRODUCTS_DIR}/DeviceKit-iOS/DeviceKit.framework", "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework", "${BUILT_PRODUCTS_DIR}/EasyTipView/EasyTipView.framework", @@ -10387,7 +10322,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CRToast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DeviceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EasyTipView.framework", @@ -10520,7 +10454,6 @@ "${BUILT_PRODUCTS_DIR}/CRToast/CRToast.framework", "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack-iOS/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework", "${BUILT_PRODUCTS_DIR}/DeviceKit-iOS/DeviceKit.framework", "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework", "${BUILT_PRODUCTS_DIR}/EasyTipView/EasyTipView.framework", @@ -10565,7 +10498,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CRToast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DeviceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EasyTipView.framework", @@ -10932,7 +10864,6 @@ "${BUILT_PRODUCTS_DIR}/CRToast/CRToast.framework", "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack-iOS/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework", "${BUILT_PRODUCTS_DIR}/DeviceKit-iOS/DeviceKit.framework", "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework", "${BUILT_PRODUCTS_DIR}/EasyTipView/EasyTipView.framework", @@ -10977,7 +10908,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CRToast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DeviceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EasyTipView.framework", @@ -11050,7 +10980,6 @@ "${BUILT_PRODUCTS_DIR}/CRToast/CRToast.framework", "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack-iOS/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework", "${BUILT_PRODUCTS_DIR}/DeviceKit-iOS/DeviceKit.framework", "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework", "${BUILT_PRODUCTS_DIR}/EasyTipView/EasyTipView.framework", @@ -11095,7 +11024,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CRToast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DeviceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EasyTipView.framework", @@ -11201,7 +11129,6 @@ "${BUILT_PRODUCTS_DIR}/CRToast/CRToast.framework", "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack-iOS/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework", "${BUILT_PRODUCTS_DIR}/DeviceKit-iOS/DeviceKit.framework", "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework", "${BUILT_PRODUCTS_DIR}/EasyTipView/EasyTipView.framework", @@ -11242,7 +11169,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CRToast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DeviceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EasyTipView.framework", @@ -11366,7 +11292,6 @@ "${BUILT_PRODUCTS_DIR}/CRToast/CRToast.framework", "${BUILT_PRODUCTS_DIR}/Charts/Charts.framework", "${BUILT_PRODUCTS_DIR}/CocoaLumberjack-iOS/CocoaLumberjack.framework", - "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework", "${BUILT_PRODUCTS_DIR}/DeviceKit-iOS/DeviceKit.framework", "${BUILT_PRODUCTS_DIR}/DownloadButton/DownloadButton.framework", "${BUILT_PRODUCTS_DIR}/EasyTipView/EasyTipView.framework", @@ -11409,7 +11334,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CRToast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Charts.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaLumberjack.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DeviceKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DownloadButton.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EasyTipView.framework", @@ -11528,12 +11452,10 @@ 08D1207D1C937B2200A54ABC /* LabelExtension.swift in Sources */, 08E8F9681F34DD2C008CF4A1 /* SearchQueriesPresenter.swift in Sources */, 0859AEA21E4F22B500A0D206 /* FillBlanksInputTableViewCell.swift in Sources */, - 08D1207E1C937B2200A54ABC /* PlaceholderView.swift in Sources */, 08D1207F1C937B2200A54ABC /* Progress.swift in Sources */, 0834C46A1E2CE469002F8516 /* MatchingDataset.swift in Sources */, 089574571E5B76E700C12D21 /* UIImageView+SVGDownload.swift in Sources */, 08D5F5781F7DA8CB007C1634 /* CourseReviewSummary+CoreDataProperties.swift in Sources */, - 08D120801C937B2200A54ABC /* PlaceholderStyleExtensions.swift in Sources */, 0864949B1E8C78A20083E0BE /* SbStepicApplicationsInfo.swift in Sources */, 08D120811C937B2200A54ABC /* MathQuizViewController.swift in Sources */, 086953101D747DC0003857A2 /* RequestChain.swift in Sources */, @@ -11617,7 +11539,6 @@ 08AF59F71E6D9BE800423EFF /* RGPageViewController+UIToolbarDelegate.swift in Sources */, 08E8F9751F34E3D5008CF4A1 /* SearchQueriesViewController.swift in Sources */, 088EDCA71F9496DE0098DEC7 /* CourseListHorizontalViewController.swift in Sources */, - 08D1209B1C937B2200A54ABC /* PlaceholderViewDataSource.swift in Sources */, 080CE1561E95635A0089A27F /* ProgressesAPI.swift in Sources */, 084F7AA81E76EF690088368A /* LastStep.swift in Sources */, 0860D9161F10EA690087D61B /* InputAccessoryBuilder.swift in Sources */, @@ -11635,7 +11556,6 @@ 08D120A01C937B2200A54ABC /* TeachersTableViewCell.swift in Sources */, 082BE3B31E67686B006BC60F /* RoutingManager.swift in Sources */, 080CE15C1E95804C0089A27F /* SearchResultsAPI.swift in Sources */, - 08D120A11C937B2200A54ABC /* PlaceholderViewDelegate.swift in Sources */, 08AF59F11E6D9BE800423EFF /* RGPageViewController+UIPageViewControllerDataSource.swift in Sources */, 0888D10A1F1E42A000A16863 /* CodeElementsSize.swift in Sources */, 08D120A21C937B2200A54ABC /* SortingQuizTableViewCell.swift in Sources */, @@ -11738,7 +11658,6 @@ 0846B1161EDDF63200D64D77 /* CodeTemplate.swift in Sources */, 08CB0D321FB5F9FC001A1E02 /* ExploreViewController.swift in Sources */, 089504841F27C5C600EEC939 /* FullHeightTableView.swift in Sources */, - 08D120CC1C937B2200A54ABC /* PlaceholderTestViewController.swift in Sources */, 08D120CD1C937B2200A54ABC /* StepicVideoPlayerViewController.swift in Sources */, 0829B83B1E9D05AE009B4A84 /* Certificate.swift in Sources */, 08D120CE1C937B2200A54ABC /* StringDatasetExtension.swift in Sources */, @@ -11835,7 +11754,6 @@ 0859AEA91E4F26C700A0D206 /* FillBlanksTextTableViewCell.swift in Sources */, 08D120F11C937B2200A54ABC /* Meta.swift in Sources */, 08D120F31C937B2200A54ABC /* StepicToken.swift in Sources */, - 08D120F41C937B2200A54ABC /* Styles.swift in Sources */, 08C1BF321FBA0CA1008F342F /* SearchResultsViewController.swift in Sources */, 080217841F55B1B200186245 /* Menu.swift in Sources */, 08D120F51C937B2200A54ABC /* SectionsViewController.swift in Sources */, @@ -11911,6 +11829,7 @@ 08AF59FC1E6D9BE800423EFF /* RGPageViewControllerDelegate.swift in Sources */, 086A9D621C74AF90003611DC /* GlobalFunctions.swift in Sources */, 08EB85E91D0F649700E4F345 /* CellOperationsUtil.swift in Sources */, + 866AD0D62061A7D70004C2B2 /* ControllerWithStepikPlaceholder.swift in Sources */, 08AF59F21E6D9BE800423EFF /* RGPageViewController+UIPageViewControllerDelegate.swift in Sources */, 0846B1101EDDEE8E00D64D77 /* CodeLimit+CoreDataProperties.swift in Sources */, 2C98B6B61FDFD74C005AB72C /* OnboardingViewController.swift in Sources */, @@ -11928,12 +11847,10 @@ 08CA59EA1BBD3D55008DC44D /* LabelExtension.swift in Sources */, 08FEFC211F127470005CA0FB /* AutocompleteWords.swift in Sources */, 0861E6721CD80A9600B45652 /* Executable.swift in Sources */, - 08970ECF1C6A326900846119 /* PlaceholderView.swift in Sources */, 083AABE91BE8D63D005E1E96 /* Progress.swift in Sources */, 86BB7C022019538100063538 /* CongratsView.swift in Sources */, 08E8F96C1F34DD48008CF4A1 /* SearchQueriesView.swift in Sources */, 0895A13B1E43836B00FE22DD /* FillBlanksReply.swift in Sources */, - 08970ED31C6A326900846119 /* PlaceholderStyleExtensions.swift in Sources */, 08F485AC1C580DB3000165AA /* MathQuizViewController.swift in Sources */, 088FD8171FB242B3008A2953 /* CourseSubscriber.swift in Sources */, 08B062DD1FDEFD5900A6C999 /* StreaksAlertPresentationManager.swift in Sources */, @@ -11991,6 +11908,7 @@ 080CE1461E9560EB0089A27F /* SectionsAPI.swift in Sources */, 083F2B1A1E9D920500714173 /* CertificatesPresenter.swift in Sources */, 08DF1D981BDAE11500BA35EA /* HTMLBuilder.swift in Sources */, + 866AD0D4206145DC0004C2B2 /* StepikPlaceholderStyle+Placeholders.swift in Sources */, 08EB85F51D10267800E4F345 /* WriteCommentDelegate.swift in Sources */, 08DF1D941BDADB2D00BA35EA /* Scripts.swift in Sources */, 0861E67B1CD9483500B45652 /* ExecutionQueues.swift in Sources */, @@ -12044,7 +11962,6 @@ 08EB85DF1D0F192900E4F345 /* LoadMoreTableViewCell.swift in Sources */, 08F5554E1C4F924000C877E8 /* Attempt.swift in Sources */, 08AC21481CDD558500FBB9CD /* PersistentUserTokenRecoveryManager.swift in Sources */, - 08970ED01C6A326900846119 /* PlaceholderViewDataSource.swift in Sources */, 2CB9E8C61F7E833F0004E17F /* NotificationsPagerViewController.swift in Sources */, 2CD8463F1F25F7F300E8153C /* Profile.swift in Sources */, 0800B81F1D06FDE5006C987E /* DiscussionProxiesAPI.swift in Sources */, @@ -12067,7 +11984,6 @@ 08B9770C1D19D5AA00FFC52C /* DiscussionWebTableViewCell.swift in Sources */, 08A0218E1D675B4700915679 /* SectionNavigationDelegate.swift in Sources */, 08CA59E61BBD25DC008DC44D /* TeachersTableViewCell.swift in Sources */, - 08970ED11C6A326900846119 /* PlaceholderViewDelegate.swift in Sources */, 08F485B71C58ECE3000165AA /* SortingQuizTableViewCell.swift in Sources */, 089A0DA71BE9FFCE004AF4EB /* UIViewExtensions.swift in Sources */, 0828FF6E1BC5D177000AFEA7 /* Section+CoreDataProperties.swift in Sources */, @@ -12198,7 +12114,6 @@ 08F485AA1C580D61000165AA /* MathReply.swift in Sources */, 08AF59F61E6D9BE800423EFF /* RGPageViewController+UIToolbarDelegate.swift in Sources */, 2CB62BDB2019ECB800B5E336 /* OnboardingCardStepViewController.swift in Sources */, - 08970ED71C6A36E500846119 /* PlaceholderTestViewController.swift in Sources */, 089C88DE1FCF494300003B63 /* CourseListType.swift in Sources */, 08EF9A081C91D0F800433E4A /* StepicVideoPlayerViewController.swift in Sources */, 08F555561C4F9FAB00C877E8 /* StringDatasetExtension.swift in Sources */, @@ -12292,7 +12207,6 @@ 083F2B171E9D8F1D00714173 /* CertificatesView.swift in Sources */, 080F31DD1BA7162C00F356A0 /* StepicToken.swift in Sources */, 080CE14F1E9562F30089A27F /* StepsAPI.swift in Sources */, - 08970ED21C6A326900846119 /* Styles.swift in Sources */, 0828FF791BC5E744000AFEA7 /* SectionsViewController.swift in Sources */, 08E3B9671EEA16DC0072995B /* CodeReply.swift in Sources */, 2CA9D9852010EEA2007AA743 /* AdaptiveRatingsAPI.swift in Sources */, @@ -12682,16 +12596,10 @@ 2C1B60EB1F4C4AEF00236804 /* RateAppAlertManager.swift in Sources */, 2C1B60EC1F4C4AEF00236804 /* Reachability.m in Sources */, 2C1B60ED1F4C4AEF00236804 /* ControllerHelperLaunchExtension.swift in Sources */, - 2C1B60EE1F4C4AEF00236804 /* PlaceholderTestViewController.swift in Sources */, - 2C1B60EF1F4C4AEF00236804 /* PlaceholderView.swift in Sources */, 2C1B60F01F4C4AEF00236804 /* CodeSnippetSymbols.swift in Sources */, - 2C1B60F11F4C4AEF00236804 /* PlaceholderViewDataSource.swift in Sources */, 2C49190B2062A1CC003E733D /* StepikTableView.swift in Sources */, 2C608AF12045691E006870BB /* StepReversedCardView.swift in Sources */, - 2C1B60F21F4C4AEF00236804 /* PlaceholderViewDelegate.swift in Sources */, 2C715DA220568BDD0098707D /* PinsMapView.swift in Sources */, - 2C1B60F31F4C4AEF00236804 /* Styles.swift in Sources */, - 2C1B60F41F4C4AEF00236804 /* PlaceholderStyleExtensions.swift in Sources */, 2C1B60F51F4C4AEF00236804 /* VideoDownloadView.swift in Sources */, 2C1B60F61F4C4AEF00236804 /* CodeSample.swift in Sources */, 2C1B60F71F4C4AEF00236804 /* AdaptiveRatingManager.swift in Sources */, @@ -12712,6 +12620,7 @@ 086FC6AE1FE04DBD00C7DFF4 /* RangeExtension.swift in Sources */, 089C88DB1FCCDF3900003B63 /* UserActivityHomeView.swift in Sources */, 2C47A168206284B1003E87EC /* NotificationRequestAlertContext.swift in Sources */, + 86A1C3392065173800D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */, 2C1B61061F4C4AEF00236804 /* Player.swift in Sources */, 2C1B61071F4C4AEF00236804 /* VideoRate.swift in Sources */, 2C1B61081F4C4AEF00236804 /* DownloadsViewController.swift in Sources */, @@ -12905,6 +12814,7 @@ 2C1B61AE1F4C4AEF00236804 /* VersionUpdateAlertConstructor.swift in Sources */, 2C1B61AF1F4C4AEF00236804 /* Version.swift in Sources */, 08CB0D531FB63F83001A1E02 /* ContentLanguagesView.swift in Sources */, + 86A1C334206515B300D0914C /* ControllerWithStepikPlaceholder.swift in Sources */, 2C1B61B01F4C4AEF00236804 /* StreaksNotificationSuggestionManager.swift in Sources */, 2C1B61B21F4C4AEF00236804 /* Executable.swift in Sources */, 2C608B282045699C006870BB /* AdaptiveCourseSelectPresenter.swift in Sources */, @@ -13165,16 +13075,10 @@ 2C1B62FC1F4C590700236804 /* RateAppAlertManager.swift in Sources */, 2C1B62FD1F4C590700236804 /* Reachability.m in Sources */, 2C1B62FE1F4C590700236804 /* ControllerHelperLaunchExtension.swift in Sources */, - 2C1B62FF1F4C590700236804 /* PlaceholderTestViewController.swift in Sources */, - 2C1B63001F4C590700236804 /* PlaceholderView.swift in Sources */, 2C1B63011F4C590700236804 /* CodeSnippetSymbols.swift in Sources */, - 2C1B63021F4C590700236804 /* PlaceholderViewDataSource.swift in Sources */, 2C49190C2062A1CC003E733D /* StepikTableView.swift in Sources */, 2C608AF22045691F006870BB /* StepReversedCardView.swift in Sources */, - 2C1B63031F4C590700236804 /* PlaceholderViewDelegate.swift in Sources */, 2C715DA120568BDD0098707D /* PinsMapView.swift in Sources */, - 2C1B63041F4C590700236804 /* Styles.swift in Sources */, - 2C1B63051F4C590700236804 /* PlaceholderStyleExtensions.swift in Sources */, 2C1B63061F4C590700236804 /* VideoDownloadView.swift in Sources */, 2C1B63071F4C590700236804 /* CodeSample.swift in Sources */, 2C1B63081F4C590700236804 /* AdaptiveRatingManager.swift in Sources */, @@ -13195,6 +13099,7 @@ 086FC6AF1FE04DBD00C7DFF4 /* RangeExtension.swift in Sources */, 089C88DC1FCCDF3900003B63 /* UserActivityHomeView.swift in Sources */, 2C47A169206284B1003E87EC /* NotificationRequestAlertContext.swift in Sources */, + 86A1C33A2065173900D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */, 2C1B63171F4C590700236804 /* Player.swift in Sources */, 2C1B63181F4C590700236804 /* VideoRate.swift in Sources */, 2C1B63191F4C590700236804 /* DownloadsViewController.swift in Sources */, @@ -13388,6 +13293,7 @@ 2C1B63BF1F4C590700236804 /* VersionUpdateAlertConstructor.swift in Sources */, 2C1B63C01F4C590700236804 /* Version.swift in Sources */, 08CB0D541FB63F83001A1E02 /* ContentLanguagesView.swift in Sources */, + 86A1C335206515B400D0914C /* ControllerWithStepikPlaceholder.swift in Sources */, 2C1B63C11F4C590700236804 /* StreaksNotificationSuggestionManager.swift in Sources */, 2C1B63C31F4C590700236804 /* Executable.swift in Sources */, 2C608B272045699C006870BB /* AdaptiveCourseSelectPresenter.swift in Sources */, @@ -13647,13 +13553,7 @@ 2C89A97F1F4C289900227C3B /* RateAppAlertManager.swift in Sources */, 2C89A9801F4C289900227C3B /* Reachability.m in Sources */, 2C89A9811F4C289900227C3B /* ControllerHelperLaunchExtension.swift in Sources */, - 2C89A9821F4C289900227C3B /* PlaceholderTestViewController.swift in Sources */, - 2C89A9831F4C289900227C3B /* PlaceholderView.swift in Sources */, 2C89A9841F4C289900227C3B /* CodeSnippetSymbols.swift in Sources */, - 2C89A9851F4C289900227C3B /* PlaceholderViewDataSource.swift in Sources */, - 2C89A9861F4C289900227C3B /* PlaceholderViewDelegate.swift in Sources */, - 2C89A9871F4C289900227C3B /* Styles.swift in Sources */, - 2C89A9881F4C289900227C3B /* PlaceholderStyleExtensions.swift in Sources */, 2C89A9891F4C289900227C3B /* VideoDownloadView.swift in Sources */, 2C89A98A1F4C289900227C3B /* CodeSample.swift in Sources */, 2C89A98B1F4C289900227C3B /* AdaptiveRatingManager.swift in Sources */, @@ -13733,6 +13633,7 @@ 2C89A9C91F4C289900227C3B /* RGPageViewController+UICollectionViewDelegate.swift in Sources */, 2C89A9CA1F4C289900227C3B /* RGPageViewController+UICollectionViewDelegateFlowLayout.swift in Sources */, 2C89A9CB1F4C289900227C3B /* CodeTemplate+CoreDataProperties.swift in Sources */, + 86A1C3372065173700D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */, 088EDCAA1F9496DE0098DEC7 /* CourseListHorizontalViewController.swift in Sources */, 2C6A76392045CE8200C509A6 /* CardStepDelegate.swift in Sources */, 2C89A9CC1F4C289900227C3B /* RGPageViewController+UIPageViewControllerDataSource.swift in Sources */, @@ -13972,6 +13873,7 @@ 2C89AA8F1F4C289900227C3B /* User+CoreDataProperties.swift in Sources */, 2C89AA901F4C289900227C3B /* User.swift in Sources */, 2C89AA921F4C289900227C3B /* Parser.swift in Sources */, + 86A1C332206515B200D0914C /* ControllerWithStepikPlaceholder.swift in Sources */, 089C88E21FCF494300003B63 /* CourseListType.swift in Sources */, 2C89AA931F4C289900227C3B /* StepicToken.swift in Sources */, 2C89AA941F4C289900227C3B /* AuthInfo.swift in Sources */, @@ -14130,16 +14032,10 @@ 2C9731331F4C38F600AC9301 /* RateAppAlertManager.swift in Sources */, 2C9731341F4C38F600AC9301 /* Reachability.m in Sources */, 2C9731351F4C38F600AC9301 /* ControllerHelperLaunchExtension.swift in Sources */, - 2C9731361F4C38F600AC9301 /* PlaceholderTestViewController.swift in Sources */, - 2C9731371F4C38F600AC9301 /* PlaceholderView.swift in Sources */, 2C9731381F4C38F600AC9301 /* CodeSnippetSymbols.swift in Sources */, - 2C9731391F4C38F600AC9301 /* PlaceholderViewDataSource.swift in Sources */, 2C49190A2062A1CC003E733D /* StepikTableView.swift in Sources */, 2C608AF02045691E006870BB /* StepReversedCardView.swift in Sources */, - 2C97313A1F4C38F600AC9301 /* PlaceholderViewDelegate.swift in Sources */, 2C715DA320568BDE0098707D /* PinsMapView.swift in Sources */, - 2C97313B1F4C38F600AC9301 /* Styles.swift in Sources */, - 2C97313C1F4C38F600AC9301 /* PlaceholderStyleExtensions.swift in Sources */, 2C97313D1F4C38F600AC9301 /* VideoDownloadView.swift in Sources */, 2C97313E1F4C38F600AC9301 /* CodeSample.swift in Sources */, 2C97313F1F4C38F600AC9301 /* AdaptiveRatingManager.swift in Sources */, @@ -14160,6 +14056,7 @@ 086FC6AD1FE04DBD00C7DFF4 /* RangeExtension.swift in Sources */, 089C88DA1FCCDF3900003B63 /* UserActivityHomeView.swift in Sources */, 2C47A167206284B1003E87EC /* NotificationRequestAlertContext.swift in Sources */, + 86A1C3382065173800D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */, 2C97314E1F4C38F600AC9301 /* Player.swift in Sources */, 2C97314F1F4C38F600AC9301 /* VideoRate.swift in Sources */, 2C9731501F4C38F600AC9301 /* DownloadsViewController.swift in Sources */, @@ -14353,6 +14250,7 @@ 2C9731F71F4C38F600AC9301 /* Version.swift in Sources */, 08CB0D521FB63F83001A1E02 /* ContentLanguagesView.swift in Sources */, 2C9731F81F4C38F600AC9301 /* StreaksNotificationSuggestionManager.swift in Sources */, + 86A1C333206515B300D0914C /* ControllerWithStepikPlaceholder.swift in Sources */, 2C9731FA1F4C38F600AC9301 /* Executable.swift in Sources */, 2C608B292045699C006870BB /* AdaptiveCourseSelectPresenter.swift in Sources */, 2C9731FB1F4C38F600AC9301 /* ExecutionQueue.swift in Sources */, @@ -14682,13 +14580,7 @@ 864514F41E9FD4F6007F73BE /* RateAppAlertManager.swift in Sources */, 864D67D61E83E394001E8D9E /* Reachability.m in Sources */, 864D67D51E83E23B001E8D9E /* ControllerHelperLaunchExtension.swift in Sources */, - 864D67411E83E08E001E8D9E /* PlaceholderTestViewController.swift in Sources */, - 864D67431E83E08E001E8D9E /* PlaceholderView.swift in Sources */, 085E9E6B1F138C1A00D6A4BC /* CodeSnippetSymbols.swift in Sources */, - 864D67441E83E08E001E8D9E /* PlaceholderViewDataSource.swift in Sources */, - 864D67451E83E08E001E8D9E /* PlaceholderViewDelegate.swift in Sources */, - 864D67461E83E08E001E8D9E /* Styles.swift in Sources */, - 864D67471E83E08E001E8D9E /* PlaceholderStyleExtensions.swift in Sources */, 864D67481E83E08E001E8D9E /* VideoDownloadView.swift in Sources */, 080C5E7D1EFC13ED0036EB3D /* CodeSample.swift in Sources */, 2CA2E72620237E55001DC410 /* AdaptiveRatingsViewController.swift in Sources */, @@ -14719,6 +14611,7 @@ 864D675B1E83E08E001E8D9E /* Player.swift in Sources */, 2CA2E72320237E4A001DC410 /* AdaptiveStatsPresenter.swift in Sources */, 864D675C1E83E08E001E8D9E /* VideoRate.swift in Sources */, + 86A1C3362065173700D0914C /* StepikPlaceholderStyle+Placeholders.swift in Sources */, 864D675D1E83E08E001E8D9E /* DownloadsViewController.swift in Sources */, 864D675E1E83E08E001E8D9E /* DownloadTableViewCell.swift in Sources */, 864D67611E83E08E001E8D9E /* DiscussionWebTableViewCell.swift in Sources */, @@ -15038,6 +14931,7 @@ 864D67111E83DE03001E8D9E /* StepicsAPI.swift in Sources */, 086D5B54201283C2000F7715 /* TooltipFactory.swift in Sources */, 2CA2E71720237E0E001DC410 /* StepReversedCardView.swift in Sources */, + 86A1C331206515B200D0914C /* ControllerWithStepikPlaceholder.swift in Sources */, 864D67121E83DE03001E8D9E /* UnitsAPI.swift in Sources */, 864D67131E83DE03001E8D9E /* UserActivitiesAPI.swift in Sources */, 089C88E01FCF494300003B63 /* CourseListType.swift in Sources */, diff --git a/Stepic/Base.lproj/Main.storyboard b/Stepic/Base.lproj/Main.storyboard index 85f3ed671a..d399bdc72e 100644 --- a/Stepic/Base.lproj/Main.storyboard +++ b/Stepic/Base.lproj/Main.storyboard @@ -254,7 +254,7 @@ - + @@ -297,7 +297,7 @@ - + @@ -690,6 +690,6 @@ - + diff --git a/Stepic/CardsStepsViewController.swift b/Stepic/CardsStepsViewController.swift index 279a106184..4b2aa21446 100644 --- a/Stepic/CardsStepsViewController.swift +++ b/Stepic/CardsStepsViewController.swift @@ -10,8 +10,9 @@ import Foundation import PromiseKit import Koloda -class CardsStepsViewController: UIViewController, CardsStepsView { +class CardsStepsViewController: UIViewController, CardsStepsView, ControllerWithStepikPlaceholder { + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() var presenter: CardsStepsPresenter? @IBOutlet weak var kolodaView: KolodaView! @@ -25,34 +26,32 @@ class CardsStepsViewController: UIViewController, CardsStepsView { didSet { switch state { case .normal: - self.placeholderView.isHidden = true - self.kolodaView.isHidden = false - case .connectionError, .coursePassed: - self.placeholderView.isHidden = false - self.kolodaView.isHidden = true - - self.placeholderView.datasource = self + isPlaceholderShown = false + case .connectionError: + showPlaceholder(for: .connectionError) + case .coursePassed: + showPlaceholder(for: .adaptiveCoursePassed) default: break } } } - lazy var placeholderView: PlaceholderView = { - let v = PlaceholderView() - self.view.insertSubview(v, aboveSubview: self.view) - v.align(toView: self.kolodaView) - v.delegate = self - v.datasource = self - v.backgroundColor = self.view.backgroundColor - return v - }() - // Can be overriden in the children classes (for adaptive app) var cardView: StepCardView { return StepCardView() } + override func viewDidLoad() { + super.viewDidLoad() + + registerPlaceholder(placeholder: StepikPlaceholder(.noConnection, action: { [weak self] in + self?.presenter?.tryAgain() + }), for: .connectionError) + + registerPlaceholder(placeholder: StepikPlaceholder(.adaptiveCoursePassed), for: .adaptiveCoursePassed) + } + func refreshCards() { if kolodaView.delegate == nil { kolodaView.dataSource = self @@ -199,62 +198,6 @@ extension CardsStepsViewController: KolodaViewDataSource { } } -extension CardsStepsViewController: PlaceholderViewDataSource { - func placeholderImage() -> UIImage? { - switch state { - case .connectionError: - return Images.placeholders.connectionError - case .coursePassed: - return Images.placeholders.coursePassed - default: - return nil - } - } - - func placeholderButtonTitle() -> String? { - switch state { - case .connectionError: - return NSLocalizedString("TryAgain", comment: "") - default: - return nil - } - } - - func placeholderDescription() -> String? { - switch state { - case .connectionError: - return nil - case .coursePassed: - return NSLocalizedString("NoRecommendations", comment: "") - default: - return nil - } - } - - func placeholderStyle() -> PlaceholderStyle { - var style = PlaceholderStyle() - style.button.textColor = UIColor.mainDark - return style - } - - func placeholderTitle() -> String? { - switch state { - case .connectionError: - return NSLocalizedString("ConnectionErrorText", comment: "") - case .coursePassed: - return NSLocalizedString("CoursePassed", comment: "") - default: - return nil - } - } -} - -extension CardsStepsViewController: PlaceholderViewDelegate { - func placeholderButtonDidPress() { - presenter?.tryAgain() - } -} - extension CardsStepsViewController: CardStepDelegate { func stepSubmissionDidCorrect() { AnalyticsReporter.reportEvent(AnalyticsEvents.Adaptive.Step.correctAnswer) diff --git a/Stepic/CertificatesStoryboard.storyboard b/Stepic/CertificatesStoryboard.storyboard index 50ad83285c..013194212c 100644 --- a/Stepic/CertificatesStoryboard.storyboard +++ b/Stepic/CertificatesStoryboard.storyboard @@ -34,7 +34,7 @@ - + diff --git a/Stepic/CertificatesViewController.swift b/Stepic/CertificatesViewController.swift index 1878eacb22..a335c552a8 100644 --- a/Stepic/CertificatesViewController.swift +++ b/Stepic/CertificatesViewController.swift @@ -7,24 +7,17 @@ // import Foundation -import DZNEmptyDataSet -enum CertificatesEmptyDatasetState { - case anonymous, error, empty, refreshing -} +class CertificatesViewController: UIViewController, CertificatesView, ControllerWithStepikPlaceholder { + + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() -class CertificatesViewController: UIViewController, CertificatesView { - @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var tableView: StepikTableView! var presenter: CertificatesPresenter? var certificates: [CertificateViewData] = [] var showNextPageFooter: Bool = false - var emptyState: CertificatesEmptyDatasetState = .empty { - didSet { - tableView.reloadEmptyDataSet() - } - } let refreshControl = UIRefreshControl() @@ -33,14 +26,26 @@ class CertificatesViewController: UIViewController, CertificatesView { tableView.delegate = self tableView.dataSource = self + tableView.emptySetPlaceholder = StepikPlaceholder(.emptyCertificates) { [weak self] in + self?.tabBarController?.selectedIndex = 1 + } + tableView.loadingPlaceholder = StepikPlaceholder(.emptyCertificatesLoading) + + registerPlaceholder(placeholder: StepikPlaceholder(.login, action: { [weak self] in + guard let strongSelf = self else { + return + } + RoutingManager.auth.routeFrom(controller: strongSelf, success: nil, cancel: nil) + }), for: .anonymous) + registerPlaceholder(placeholder: StepikPlaceholder(.noConnection, action: { [weak self] in + self?.presenter?.checkStatus() + }), for: .connectionError) + title = NSLocalizedString("Certificates", comment: "") presenter = CertificatesPresenter(certificatesAPI: ApiDataDownloader.certificates, coursesAPI: ApiDataDownloader.courses, presentationContainer: PresentationContainer.certificates, view: self) presenter?.view = self - tableView.emptyDataSetSource = self - tableView.emptyDataSetDelegate = self - tableView.register(UINib(nibName: "CertificateTableViewCell", bundle: nil), forCellReuseIdentifier: "CertificateTableViewCell") self.tableView.estimatedRowHeight = 161 @@ -125,21 +130,23 @@ class CertificatesViewController: UIViewController, CertificatesView { func displayAnonymous() { refreshControl.endRefreshing() - emptyState = .anonymous + showPlaceholder(for: .anonymous) } func displayError() { refreshControl.endRefreshing() - emptyState = .error + showPlaceholder(for: .connectionError) } func displayEmpty() { refreshControl.endRefreshing() - emptyState = .empty + tableView.reloadData() + self.isPlaceholderShown = false } func displayRefreshing() { - emptyState = .refreshing + tableView.showLoadingPlaceholder() + self.isPlaceholderShown = false } func displayLoadNextPageError() { @@ -172,30 +179,6 @@ class CertificatesViewController: UIViewController, CertificatesView { } } -extension CertificatesViewController : DZNEmptyDataSetDelegate { - func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool { - return true - } - - func emptyDataSetDidTapButton(_ scrollView: UIScrollView!) { - switch emptyState { - case .anonymous: - RoutingManager.auth.routeFrom(controller: self, success: nil, cancel: nil) - break - - case .empty: - self.tabBarController?.selectedIndex = 1 - break - - case .error: - break - - case .refreshing: - break - } - } -} - extension CertificatesViewController : UITableViewDelegate { func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { @@ -272,96 +255,3 @@ extension CertificatesViewController : UITableViewDataSource { return cell } } - -extension CertificatesViewController : DZNEmptyDataSetSource { - func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! { - //Add correct placeholders here - switch emptyState { - case .anonymous: - return Images.placeholders.anonymous - case .empty: - return Images.placeholders.certificates - case .error: - return Images.placeholders.connectionError - case .refreshing: - return Images.placeholders.certificates - } - } - - func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - - switch emptyState { - case .anonymous: - text = NSLocalizedString("AnonymousCertificatesTitle", comment: "") - case .empty: - text = NSLocalizedString("EmptyCertificatesTitle", comment: "") - break - case .error: - text = NSLocalizedString("ConnectionErrorTitle", comment: "") - break - case .refreshing: - text = NSLocalizedString("Refreshing", comment: "") - break - } - - let attributes = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18.0), - NSAttributedStringKey.foregroundColor: UIColor.darkGray] - - return NSAttributedString(string: text, attributes: attributes) - } - - func description(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - - switch emptyState { - case .anonymous: - text = NSLocalizedString("SignInToHaveCertificates", comment: "") - break - case .empty: - text = NSLocalizedString("EmptyCertificatesDescription", comment: "") - break - case .error: - text = NSLocalizedString("ConnectionErrorPullToRefresh", comment: "") - break - case .refreshing: - text = NSLocalizedString("RefreshingDescription", comment: "") - break - } - - let paragraph = NSMutableParagraphStyle() - paragraph.lineBreakMode = .byWordWrapping - paragraph.alignment = .center - - let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14.0), - NSAttributedStringKey.foregroundColor: UIColor.lightGray, - NSAttributedStringKey.paragraphStyle: paragraph] - - return NSAttributedString(string: text, attributes: attributes) - } - - func buttonTitle(forEmptyDataSet scrollView: UIScrollView!, for state: UIControlState) -> NSAttributedString! { - var text: String = "" - switch emptyState { - case .anonymous: - text = NSLocalizedString("SignIn", comment: "") - case .empty: - text = NSLocalizedString("ChooseCourse", comment: "") - case .error: - text = "" - break - case .refreshing: - text = "" - break - } - - let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16.0), - NSAttributedStringKey.foregroundColor: UIColor.mainDark] - - return NSAttributedString(string: text, attributes: attributes) - } - - func backgroundColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! { - return UIColor.groupTableViewBackground - } -} diff --git a/Stepic/ControllerWithStepikPlaceholder.swift b/Stepic/ControllerWithStepikPlaceholder.swift new file mode 100644 index 0000000000..2a79724da9 --- /dev/null +++ b/Stepic/ControllerWithStepikPlaceholder.swift @@ -0,0 +1,106 @@ +// +// ControllerWithStepikPlaceholder.swift +// Stepic +// +// Created by Vladislav Kiryukhin on 20.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import UIKit + +typealias StepikPlaceholderControllerState = StepikPlaceholderControllerContainer.PlaceholderState + +class StepikPlaceholderControllerContainer: StepikPlaceholderViewDelegate { + static let shared = StepikPlaceholderControllerContainer() + + open class PlaceholderState: Equatable, Hashable { + var id: String + + init(id: String) { + self.id = id + } + + static let anonymous = PlaceholderState(id: "anonymous") + static let connectionError = PlaceholderState(id: "connectionError") + static let refreshing = PlaceholderState(id: "refreshing") + static let adaptiveCoursePassed = PlaceholderState(id: "adaptiveCoursePassed") + + var hashValue: Int { + get { + return id.hashValue + } + } + + public static func == (lhs: PlaceholderState, rhs: PlaceholderState) -> Bool { + return lhs.id == rhs.id + } + } + + var registeredPlaceholders: [PlaceholderState: StepikPlaceholder] = [:] + var currentPlaceholderButtonAction: (() -> Void)? + var isPlaceholderShown: Bool = false + + lazy var placeholderView: StepikPlaceholderView = { + let view = StepikPlaceholderView() + return view + }() + + func buttonDidClick(_ button: UIButton) { + currentPlaceholderButtonAction?() + } +} + +protocol ControllerWithStepikPlaceholder: class { + var isPlaceholderShown: Bool { get set } + var placeholderContainer: StepikPlaceholderControllerContainer { get set } + + func registerPlaceholder(placeholder: StepikPlaceholder, for state: StepikPlaceholderControllerState) + func showPlaceholder(for state: StepikPlaceholderControllerState) +} + +extension ControllerWithStepikPlaceholder where Self: UIViewController { + var isPlaceholderShown: Bool { + set { + placeholderContainer.placeholderView.isHidden = !newValue + placeholderContainer.isPlaceholderShown = newValue + } + get { + return placeholderContainer.isPlaceholderShown + } + } + + func registerPlaceholder(placeholder: StepikPlaceholder, for state: StepikPlaceholderControllerState) { + placeholderContainer.registeredPlaceholders[state] = placeholder + } + + func showPlaceholder(for state: StepikPlaceholderControllerState) { + guard let placeholder = placeholderContainer.registeredPlaceholders[state] else { + return + } + + updatePlaceholderLayout() + placeholderContainer.placeholderView.set(placeholder: placeholder.style) + placeholderContainer.placeholderView.delegate = placeholderContainer + placeholderContainer.currentPlaceholderButtonAction = placeholder.buttonAction + + isPlaceholderShown = true + } + + private func updatePlaceholderLayout() { + guard let view = self.view else { + return + } + + if placeholderContainer.placeholderView.superview == nil { + placeholderContainer.placeholderView.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(placeholderContainer.placeholderView) + placeholderContainer.placeholderView.alignCenter(withView: view) + placeholderContainer.placeholderView.align(toView: view) + + placeholderContainer.placeholderView.setNeedsLayout() + placeholderContainer.placeholderView.layoutIfNeeded() + } + view.bringSubview(toFront: placeholderContainer.placeholderView) + } +} diff --git a/Stepic/DiscussionsViewController.swift b/Stepic/DiscussionsViewController.swift index 9f7ccd4e1b..8282b3569a 100644 --- a/Stepic/DiscussionsViewController.swift +++ b/Stepic/DiscussionsViewController.swift @@ -8,7 +8,6 @@ import UIKit import SDWebImage -import DZNEmptyDataSet enum DiscussionsEmptyDataSetState { case error, empty, none @@ -38,7 +37,8 @@ struct DiscussionsCellInfo { } } -class DiscussionsViewController: UIViewController { +class DiscussionsViewController: UIViewController, ControllerWithStepikPlaceholder { + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() var discussionProxyId: String! var target: Int! @@ -46,7 +46,7 @@ class DiscussionsViewController: UIViewController { // This var is used only for incrementing discussions count var step: Step? - @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var tableView: StepikTableView! var refreshControl: UIRefreshControl? = UIRefreshControl() @@ -54,7 +54,16 @@ class DiscussionsViewController: UIViewController { var emptyDatasetState: DiscussionsEmptyDataSetState = .none { didSet { - tableView.reloadEmptyDataSet() + switch emptyDatasetState { + case .none: + isPlaceholderShown = false + tableView.showLoadingPlaceholder() + case .empty: + isPlaceholderShown = false + tableView.reloadData() + case .error: + showPlaceholder(for: .connectionError) + } } } @@ -63,10 +72,15 @@ class DiscussionsViewController: UIViewController { print("did load") + registerPlaceholder(placeholder: StepikPlaceholder(.noConnection, action: { [weak self] in + self?.reloadDiscussions() + }), for: .connectionError) + tableView.delegate = self tableView.dataSource = self - tableView.emptyDataSetSource = self - tableView.emptyDataSetDelegate = self + tableView.emptySetPlaceholder = StepikPlaceholder(.emptyDiscussions) + tableView.loadingPlaceholder = StepikPlaceholder(.emptyDiscussionsLoading) + emptyDatasetState = .none self.tableView.rowHeight = UITableViewAutomaticDimension @@ -274,10 +288,7 @@ class DiscussionsViewController: UIViewController { UIThread.performUI({ [weak self] in if self?.cellsInfo.count == 0 { - self?.tableView.emptyDataSetSource = self self?.emptyDatasetState = emptyState - } else { - self?.tableView.emptyDataSetSource = nil } self?.tableView.reloadData() }) @@ -731,71 +742,3 @@ extension DiscussionsViewController : WriteCommentDelegate { } } } - -extension DiscussionsViewController : DZNEmptyDataSetSource, DZNEmptyDataSetDelegate { - func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! { - switch emptyDatasetState { - case .empty: - return Images.noCommentsWhite.size200x200 - case .error: - return Images.noWifiImage.white - case .none: - return Images.noCommentsWhite.size200x200 - } - } - - func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - switch emptyDatasetState { - case .empty: - text = NSLocalizedString("NoDiscussionsTitle", comment: "") - break - case .error: - text = NSLocalizedString("ConnectionErrorTitle", comment: "") - break - case .none: - text = "" - break - } - - let attributes = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18.0), - NSAttributedStringKey.foregroundColor: UIColor.darkGray] - - return NSAttributedString(string: text, attributes: attributes) - } - - func description(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - - switch emptyDatasetState { - case .empty: - text = NSLocalizedString("NoDiscussionsDescription", comment: "") - break - case .error: - text = NSLocalizedString("ConnectionErrorPullToRefresh", comment: "") - break - case .none: - text = NSLocalizedString("RefreshingDiscussions", comment: "") - break - } - - let paragraph = NSMutableParagraphStyle() - paragraph.lineBreakMode = .byWordWrapping - paragraph.alignment = .center - - let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14.0), - NSAttributedStringKey.foregroundColor: UIColor.lightGray, - NSAttributedStringKey.paragraphStyle: paragraph] - - return NSAttributedString(string: text, attributes: attributes) - } - - func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat { - // print("offset -> \((self.navigationController?.navigationBar.bounds.height) ?? 0 + UIApplication.sharedApplication().statusBarFrame.height)") - return 0 - } - - func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool { - return true - } -} diff --git a/Stepic/DiscussionsViewController.xib b/Stepic/DiscussionsViewController.xib index 85de456107..10d2564cc4 100644 --- a/Stepic/DiscussionsViewController.xib +++ b/Stepic/DiscussionsViewController.xib @@ -1,15 +1,14 @@ - + - - + - + @@ -17,10 +16,10 @@ - + - + diff --git a/Stepic/DownloadsViewController.swift b/Stepic/DownloadsViewController.swift index f0e82490fa..ae6dfa8c26 100644 --- a/Stepic/DownloadsViewController.swift +++ b/Stepic/DownloadsViewController.swift @@ -276,7 +276,6 @@ extension DownloadsViewController : VideoDownloadDelegate { tableView.deleteSections(IndexSet(integer: 0), with: .automatic) } self.tableView.endUpdates() - self.tableView.reloadEmptyDataSet() } } @@ -302,7 +301,6 @@ extension DownloadsViewController : VideoDownloadDelegate { tableView.deleteSections(IndexSet(integer: (isSectionDownloading(0) ? 1 : 0)), with: .automatic) } self.tableView.endUpdates() - self.tableView.reloadEmptyDataSet() } } diff --git a/Stepic/MenuViewController.swift b/Stepic/MenuViewController.swift index 6ee1cedfe7..3eca8468b0 100644 --- a/Stepic/MenuViewController.swift +++ b/Stepic/MenuViewController.swift @@ -11,7 +11,7 @@ import FLKAutoLayout class MenuViewController: UIViewController { - let tableView: UITableView = UITableView() + let tableView: StepikTableView = StepikTableView() var interfaceManager: MenuUIManager? var menu: Menu? { diff --git a/Stepic/NotificationsPagerViewController.swift b/Stepic/NotificationsPagerViewController.swift index 3f5d737f4a..2a401013c2 100644 --- a/Stepic/NotificationsPagerViewController.swift +++ b/Stepic/NotificationsPagerViewController.swift @@ -8,21 +8,13 @@ import UIKit -class NotificationsPagerViewController: PagerController { +class NotificationsPagerViewController: PagerController, ControllerWithStepikPlaceholder { + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() + var sections: [NotificationsSection] = [ .all, .learning, .comments, .reviews, .teaching, .other ] - lazy var placeholderView: UIView = { - let v = PlaceholderView() - self.view.addSubview(v) - v.align(toView: self.view) - v.delegate = self - v.datasource = self - v.backgroundColor = UIColor.groupTableViewBackground - return v - }() - override func viewDidLoad() { super.viewDidLoad() @@ -31,14 +23,27 @@ class NotificationsPagerViewController: PagerController { self.dataSource = self setUpTabs() + registerPlaceholder(placeholder: StepikPlaceholder(.login, action: { [weak self] in + guard let strongSelf = self else { + return + } + + RoutingManager.auth.routeFrom(controller: strongSelf, success: nil, cancel: nil) + }), for: .anonymous) + if !AuthInfo.shared.isAuthorized { - placeholderView.isHidden = false + showPlaceholder(for: .anonymous) } } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - placeholderView.isHidden = AuthInfo.shared.isAuthorized + + if AuthInfo.shared.isAuthorized { + isPlaceholderShown = false + } else { + showPlaceholder(for: .anonymous) + } updateNavigationControllerShadow(show: !AuthInfo.shared.isAuthorized) } @@ -103,34 +108,3 @@ extension NotificationsPagerViewController: UINavigationControllerDelegate { } } } - -extension NotificationsPagerViewController: PlaceholderViewDataSource { - func placeholderImage() -> UIImage? { - return Images.placeholders.anonymous - } - - func placeholderButtonTitle() -> String? { - return NSLocalizedString("SignIn", comment: "") - } - - func placeholderDescription() -> String? { - return NSLocalizedString("SignInToHaveNotifications", comment: "") - } - - func placeholderStyle() -> PlaceholderStyle { - var style = stepicPlaceholderStyle - style.button.backgroundColor = .clear - style.title.textColor = UIColor.darkGray - return style - } - - func placeholderTitle() -> String? { - return NSLocalizedString("AnonymousNotificationsTitle", comment: "") - } -} - -extension NotificationsPagerViewController: PlaceholderViewDelegate { - func placeholderButtonDidPress() { - RoutingManager.auth.routeFrom(controller: self, success: nil, cancel: nil) - } -} diff --git a/Stepic/NotificationsViewController.swift b/Stepic/NotificationsViewController.swift index 9a7b929ebb..45a0ecd589 100644 --- a/Stepic/NotificationsViewController.swift +++ b/Stepic/NotificationsViewController.swift @@ -28,7 +28,6 @@ class NotificationsViewController: UIViewController, NotificationsView { case .empty: self.refreshControl.endRefreshing() self.tableView.tableFooterView = UIView() - self.tableView.reloadEmptyDataSet() } } } diff --git a/Stepic/PlaceholderStyleExtensions.swift b/Stepic/PlaceholderStyleExtensions.swift deleted file mode 100644 index 2dcb3cf30c..0000000000 --- a/Stepic/PlaceholderStyleExtensions.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// PlaceholderStyleExtensions.swift -// OstrenkiyPlaceholderView -// -// Created by Alexander Karpov on 08.02.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - -import UIKit - -extension UIButton { - func implementStyle(_ style: PlaceholderStyle.ButtonStyle) { - self.titleLabel?.font = style.font - self.setTitleColor(style.textColor, for: UIControlState()) - switch style.borderType { - case .rect : - self.setRoundedCorners(cornerRadius: 0.0, borderWidth: 1.0, borderColor: style.borderColor) - break - case .rounded: - self.setRoundedCorners(cornerRadius: 8.0, borderWidth: 1.0, borderColor: style.borderColor) - break - case .none: - break - } - self.backgroundColor = style.backgroundColor - } -} - -extension UILabel { - func implementStyle(_ style: PlaceholderStyle.LabelStyle) { - self.font = style.font - self.textColor = style.textColor - self.textAlignment = style.textAlignment - self.lineBreakMode = style.lineBreakMode - } - - class func heightForLabelWithText(_ text: String, style: PlaceholderStyle.LabelStyle, width: CGFloat) -> CGFloat { - let label = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude)) - - label.numberOfLines = 0 - - label.text = text - - label.implementStyle(style) - label.sizeToFit() - - // print(label.bounds.height) - return label.bounds.height - } - -} diff --git a/Stepic/PlaceholderTestViewController.swift b/Stepic/PlaceholderTestViewController.swift deleted file mode 100644 index 15fe5faabb..0000000000 --- a/Stepic/PlaceholderTestViewController.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// PlaceholderTestViewController.swift -// OstrenkiyPlaceholderView -// -// Created by Alexander Karpov on 09.02.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - -import UIKit -import FLKAutoLayout - -class PlaceholderTestViewController: UIViewController { - - override func viewDidLoad() { - super.viewDidLoad() - let placeholderView = PlaceholderView() -// placeholderView.backgroundColor = UIColor.greenColor() - self.view.addSubview(placeholderView) - placeholderView.alignTop("16", leading: "16", bottom: "-16", trailing: "-16", toView: self.view) - placeholderView.delegate = self - placeholderView.datasource = self - - // Do any additional setup after loading the view, typically from a nib. - } - - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. - } - -} - -extension PlaceholderTestViewController : PlaceholderViewDelegate { - func placeholderButtonDidPress() { - print("did press placeholder button") - } -} - -extension PlaceholderTestViewController : PlaceholderViewDataSource { - func placeholderButtonTitle() -> String? { - return nil -// return "Try again" - } - - func placeholderStyle() -> PlaceholderStyle { - return stepicPlaceholderStyle - } - - func placeholderDescription() -> String? { - return "Failed to connect to the Internet. Press the button to retry or go fuck yourself" - } - - func placeholderImage() -> UIImage? { - return nil - } - - func placeholderTitle() -> String? { - return "Connection error!" - } -} diff --git a/Stepic/PlaceholderView.storyboard b/Stepic/PlaceholderView.storyboard deleted file mode 100644 index 313549f2fa..0000000000 --- a/Stepic/PlaceholderView.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Stepic/PlaceholderView.swift b/Stepic/PlaceholderView.swift deleted file mode 100644 index c8e309a35c..0000000000 --- a/Stepic/PlaceholderView.swift +++ /dev/null @@ -1,241 +0,0 @@ -// -// PlaceholderView.swift -// OstrenkiyPlaceholderView -// -// Created by Alexander Karpov on 02.02.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - -import UIKit -import FLKAutoLayout - -class PlaceholderView: UIView { - - /* - // Only override drawRect: if you perform custom drawing. - // An empty implementation adversely affects performance during animation. - override func drawRect(rect: CGRect) { - // Drawing code - } - */ - - fileprivate var bottomElement: UIView? - - fileprivate var middleView: UIView! - fileprivate var middleViewHeight: NSLayoutConstraint! - - fileprivate var imageView: UIImageView? - fileprivate var imageViewHeight: NSLayoutConstraint? - fileprivate var imageViewWidth: NSLayoutConstraint? - - fileprivate var titleLabel: UILabel? - fileprivate var titleLabelHeight: NSLayoutConstraint? - - fileprivate var descriptionLabel: UILabel? - fileprivate var descriptionLabelHeight: NSLayoutConstraint? - - fileprivate var button: UIButton? - fileprivate var buttonHeight: NSLayoutConstraint? - - fileprivate func addMiddleView() { - middleView = UIView() - self.addSubview(middleView) - self.bringSubview(toFront: middleView) - middleView.alignLeading("0", trailing: "0", toView: self) - middleView.alignCenterY(withView: self, predicate: "0") - middleViewHeight = middleView.constrainHeight("0") - middleView.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 999), for: .vertical) - setNeedsLayout() - layoutIfNeeded() - } - - fileprivate func setUpVerticalConstraints(_ view: UIView) { - if let b = bottomElement { - view.constrainTopSpace(toView: b, predicate: "16") - middleViewHeight.constant += 16 - } else { - view.alignTopEdge(withView: middleView, predicate: "0") - } - - bottomElement = view - } - - fileprivate func addImage(_ image: UIImage) { - imageView = UIImageView(frame: CGRect.zero) - middleView.addSubview(imageView!) - middleView.bringSubview(toFront: imageView!) - setUpVerticalConstraints(imageView!) - imageView?.image = image - imageViewHeight = imageView!.constrainHeight("\(image.size.height)") - imageViewWidth = imageView!.constrainWidth("\(image.size.width)") - _ = imageView?.alignCenterX(withView: middleView, predicate: "0") - } - - fileprivate func addTitle(_ title: String) { - titleLabel = UILabel(frame: CGRect.zero) - titleLabel?.text = title - titleLabel?.numberOfLines = 0 - - middleView.addSubview(titleLabel!) - middleView.bringSubview(toFront: titleLabel!) - setUpVerticalConstraints(titleLabel!) - _ = titleLabel?.alignLeading("8", trailing: "-8", toView: middleView) - - if let style = datasource?.placeholderStyle() { - titleLabel?.implementStyle(style.title) - titleLabelHeight = titleLabel?.constrainHeight("\(UILabel.heightForLabelWithText(title, style: style.title, width: middleView.bounds.width - 16))") - } else { - titleLabelHeight = titleLabel?.constrainHeight("30") - } - - //TODO: Add title style implementation here - } - - fileprivate func addDescription(_ desc: String) { - descriptionLabel = UILabel(frame: CGRect.zero) - descriptionLabel?.text = desc - descriptionLabel?.numberOfLines = 0 - - middleView.addSubview(descriptionLabel!) - middleView.bringSubview(toFront: descriptionLabel!) - setUpVerticalConstraints(descriptionLabel!) - _ = descriptionLabel?.alignLeading("8", trailing: "-8", toView: middleView) - - if let style = datasource?.placeholderStyle() { - descriptionLabel?.implementStyle(style.description) - descriptionLabelHeight = descriptionLabel?.constrainHeight("\(UILabel.heightForLabelWithText(desc, style: style.description, width: middleView.bounds.width - 16))") - } else { - descriptionLabelHeight = descriptionLabel?.constrainHeight("30") - } - - //TODO: Add description style implementation here - } - - fileprivate func addButton(_ buttonTitle: String) { - - button = UIButton(type: .system) - button?.frame = CGRect.zero - button?.setTitle(buttonTitle, for: UIControlState()) - button?.addTarget(self, action: #selector(PlaceholderView.didPressButton), for: UIControlEvents.touchUpInside) - - if let style = datasource?.placeholderStyle() { - if style.button.borderType != .none { - button?.setTitle(" \(buttonTitle) ", for: UIControlState()) - } - button?.implementStyle(style.button) - } - - middleView.addSubview(button!) - middleView.bringSubview(toFront: button!) - setUpVerticalConstraints(button!) - _ = button?.alignCenterX(withView: middleView, predicate: "0") - buttonHeight = button?.constrainHeight("30") - } - - @objc func didPressButton() { - delegate?.placeholderButtonDidPress?() - } - - fileprivate func update() { - subviews.forEach({$0.removeFromSuperview()}) - if subviews.count != 0 { - print("subviews count != 0") - } - removeConstraints(constraints) - - bottomElement = nil - - addMiddleView() - - if let image = datasource?.placeholderImage() { - addImage(image) - middleViewHeight.constant += imageViewHeight?.constant ?? 0 - } else { - imageView = nil - } - - if let title = datasource?.placeholderTitle() { - addTitle(title) - middleViewHeight.constant += titleLabelHeight?.constant ?? 0 - } else { - titleLabel = nil - } - - if let desc = datasource?.placeholderDescription() { - addDescription(desc) - middleViewHeight.constant += descriptionLabelHeight?.constant ?? 0 - } else { - descriptionLabel = nil - } - - if let btitle = datasource?.placeholderButtonTitle() { - addButton(btitle) - middleViewHeight.constant += buttonHeight?.constant ?? 0 - } else { - button = nil - } - -// if let b = bottomElement { -// b.constrainBottomSpaceToView(middleView, predicate: "0") -// } else { -// print("No items in placeholder view") -// } - - middleView.layoutSubviews() - - print("middle view height -> \(middleView.bounds.height)") - print("middle view height -> \(middleView.bounds.height)") - setNeedsLayout() - layoutIfNeeded() - middleView.layoutSubviews() - invalidateIntrinsicContentSize() - print("middle view height -> \(middleView.bounds.height)") - print("image view height -> \(String(describing: imageView?.bounds.height))") - } - - fileprivate func setup() { - let middleView = UIView() - middleView.backgroundColor = UIColor.blue - self.addSubview(middleView) - } - - var delegate: PlaceholderViewDelegate? { - didSet { - update() - } - } - var datasource: PlaceholderViewDataSource? { - didSet { - update() - } - } - - override func layoutIfNeeded() { - print("middleView frame before -> \(middleView.frame)") - super.layoutIfNeeded() - print("middleView frame after -> \(middleView.frame)") - } - - override init(frame: CGRect) { - // 1. setup any properties here - - // 2. call super.init(frame:) - super.init(frame: frame) - setup() - } - - required init?(coder aDecoder: NSCoder) { - // 1. setup any properties here - - // 2. call super.init(coder:) - super.init(coder: aDecoder) - - // 3. Setup view from .xib file - setup() - } - - override var intrinsicContentSize: CGSize { - return CGSize(width: UIViewNoIntrinsicMetric, height: 250) - } - -} diff --git a/Stepic/PlaceholderViewDataSource.swift b/Stepic/PlaceholderViewDataSource.swift deleted file mode 100644 index fb367b8707..0000000000 --- a/Stepic/PlaceholderViewDataSource.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// PlaceholderViewDataSource.swift -// OstrenkiyPlaceholderView -// -// Created by Alexander Karpov on 02.02.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - -import UIKit - -protocol PlaceholderViewDataSource { - func placeholderImage() -> UIImage? - func placeholderStyle() -> PlaceholderStyle - func placeholderTitle() -> String? - func placeholderDescription() -> String? - func placeholderButtonTitle() -> String? -} diff --git a/Stepic/PlaceholderViewDelegate.swift b/Stepic/PlaceholderViewDelegate.swift deleted file mode 100644 index 2005b925a1..0000000000 --- a/Stepic/PlaceholderViewDelegate.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// PlaceholderViewDelegate.swift -// OstrenkiyPlaceholderView -// -// Created by Alexander Karpov on 02.02.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - -import UIKit - -@objc(PlaceholderViewDelegate) -protocol PlaceholderViewDelegate { - @objc optional func placeholderButtonDidPress() -} diff --git a/Stepic/ProfileViewController.swift b/Stepic/ProfileViewController.swift index cd98d00d6a..20319b4aa9 100644 --- a/Stepic/ProfileViewController.swift +++ b/Stepic/ProfileViewController.swift @@ -8,25 +8,51 @@ import UIKit import Presentr -import DZNEmptyDataSet -class ProfileViewController: MenuViewController, ProfileView { +class ProfileViewController: MenuViewController, ProfileView, ControllerWithStepikPlaceholder { + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() var presenter: ProfilePresenter? var shareBarButtonItem: UIBarButtonItem? - var state: ProfileState = .refreshing + var state: ProfileState = .refreshing { + didSet { + switch state { + case .refreshing: + showPlaceholder(for: .refreshing) + case .anonymous: + showPlaceholder(for: .anonymous) + case .error: + showPlaceholder(for: .connectionError) + case .authorized: + isPlaceholderShown = false + } + } + } override func viewDidLoad() { super.viewDidLoad() + registerPlaceholder(placeholder: StepikPlaceholder(.login, action: { [weak self] in + guard let strongSelf = self else { + return + } + + RoutingManager.auth.routeFrom(controller: strongSelf, success: nil, cancel: nil) + }), for: .anonymous) + + registerPlaceholder(placeholder: StepikPlaceholder(.noConnection, action: { [weak self] in + self?.presenter?.updateProfile() + }), for: .connectionError) + + registerPlaceholder(placeholder: StepikPlaceholder(.emptyProfileLoading), for: .refreshing) + + state = .refreshing + presenter = ProfilePresenter(view: self, userActivitiesAPI: ApiDataDownloader.userActivities, usersAPI: ApiDataDownloader.users, notificationPermissionManager: NotificationPermissionManager()) shareBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.action, target: self, action: #selector(ProfileViewController.shareButtonPressed)) self.navigationItem.rightBarButtonItem = shareBarButtonItem! - tableView.emptyDataSetSource = self - tableView.emptyDataSetDelegate = self - if #available(iOS 11.0, *) { tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentBehavior.never } @@ -204,113 +230,3 @@ class ProfileViewController: MenuViewController, ProfileView { streaksTooltip?.dismiss() } } - -extension ProfileViewController : DZNEmptyDataSetDelegate { - @objc func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool { - return false - } - - @objc func emptyDataSetDidTapButton(_ scrollView: UIScrollView!) { - switch state { - case .anonymous: - RoutingManager.auth.routeFrom(controller: self, success: nil, cancel: nil) - break - case .error: - presenter?.updateProfile() - break - default: - break - } - } -} - -extension ProfileViewController : DZNEmptyDataSetSource { - func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! { - switch state { - case .anonymous: - return Images.placeholders.anonymous - case .error: - return Images.placeholders.connectionError - case .refreshing: - return Images.placeholders.anonymous - default: - return UIImage() - } - } - - func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - - switch state { - case .anonymous: - text = NSLocalizedString("ProfileAnonymousTitle", comment: "") - case .error: - text = NSLocalizedString("ConnectionErrorTitle", comment: "") - break - case .refreshing: - text = NSLocalizedString("Refreshing", comment: "") - break - default: - break - } - - let attributes = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18.0), - NSAttributedStringKey.foregroundColor: UIColor.darkGray] - - return NSAttributedString(string: text, attributes: attributes) - } - - func description(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - - switch state { - case .anonymous: - text = NSLocalizedString("ProfileAnonymousSubtitle", comment: "") - break - case .error: - text = "" - break - case .refreshing: - text = NSLocalizedString("RefreshingDescription", comment: "") - break - default: - break - } - - let paragraph = NSMutableParagraphStyle() - paragraph.lineBreakMode = .byWordWrapping - paragraph.alignment = .center - - let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14.0), - NSAttributedStringKey.foregroundColor: UIColor.lightGray, - NSAttributedStringKey.paragraphStyle: paragraph] - - return NSAttributedString(string: text, attributes: attributes) - } - - func buttonTitle(forEmptyDataSet scrollView: UIScrollView!, for state: UIControlState) -> NSAttributedString! { - var text: String = "" - - switch self.state { - case .anonymous: - text = NSLocalizedString("SignIn", comment: "") - case .error: - text = NSLocalizedString("TryAgain", comment: "") - break - case .refreshing: - text = "" - break - default: - break - } - - let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16.0), - NSAttributedStringKey.foregroundColor: UIColor.mainDark] - - return NSAttributedString(string: text, attributes: attributes) - } - - func backgroundColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! { - return UIColor.groupTableViewBackground - } -} diff --git a/Stepic/QuizViewController.swift b/Stepic/QuizViewController.swift index 0beb6dcc07..f3d4009e9a 100644 --- a/Stepic/QuizViewController.swift +++ b/Stepic/QuizViewController.swift @@ -9,7 +9,8 @@ import UIKit import Presentr -class QuizViewController: UIViewController, QuizView, QuizControllerDataSource { +class QuizViewController: UIViewController, QuizView, QuizControllerDataSource, ControllerWithStepikPlaceholder { + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() @IBOutlet weak var sendButtonHeight: NSLayoutConstraint! @IBOutlet weak var sendButton: UIButton! @@ -50,21 +51,6 @@ class QuizViewController: UIViewController, QuizView, QuizControllerDataSource { } private let wrongTitle: String = NSLocalizedString("Wrong", comment: "") private let peerReviewText: String = NSLocalizedString("PeerReviewText", comment: "") - fileprivate let warningViewTitle = NSLocalizedString("ConnectionErrorText", comment: "") - - private var warningView: UIView? - private func initWarningView() -> UIView { - //TODO: change warning image! - let v = PlaceholderView() - self.view.insertSubview(v, aboveSubview: self.view) - v.align(toView: self.view) - v.constrainHeight("150") - v.delegate = self - v.datasource = self - v.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1000.0), for: UILayoutConstraintAxis.vertical) - v.backgroundColor = UIColor.white - return v - } private var activityView: UIView? private func initActivityView() -> UIView { @@ -106,20 +92,10 @@ class QuizViewController: UIViewController, QuizView, QuizControllerDataSource { private var doesPresentWarningView: Bool = false { didSet { if doesPresentWarningView { - DispatchQueue.main.async { - [weak self] in - if self?.warningView == nil { - self?.warningView = self?.initWarningView() - } - self?.warningView?.isHidden = false - } + showPlaceholder(for: .connectionError) self.presenter?.delegate?.didWarningPlaceholderShow() } else { - DispatchQueue.main.async { - [weak self] in - self?.warningView?.removeFromSuperview() - self?.warningView = nil - } + isPlaceholderShown = false } } } @@ -139,6 +115,10 @@ class QuizViewController: UIViewController, QuizView, QuizControllerDataSource { override func viewDidLoad() { super.viewDidLoad() + registerPlaceholder(placeholder: StepikPlaceholder(.noConnectionQuiz, action: { [weak self] in + self?.presenter?.refreshAttempt() + }), for: .connectionError) + self.hintView.setRoundedCorners(cornerRadius: 8, borderWidth: 1, borderColor: UIColor.black) self.hintHeightWebViewHelper = CellWebViewHelper(webView: hintWebView) self.hintView.backgroundColor = UIColor.black @@ -464,34 +444,6 @@ class QuizViewController: UIViewController, QuizView, QuizControllerDataSource { } } -extension QuizViewController : PlaceholderViewDataSource { - func placeholderImage() -> UIImage? { - return Images.noWifiImage.size100x100 - } - - func placeholderButtonTitle() -> String? { - return NSLocalizedString("TryAgain", comment: "") - } - - func placeholderDescription() -> String? { - return nil - } - - func placeholderStyle() -> PlaceholderStyle { - return stepicPlaceholderStyle - } - - func placeholderTitle() -> String? { - return warningViewTitle - } -} - -extension QuizViewController : PlaceholderViewDelegate { - func placeholderButtonDidPress() { - self.presenter?.refreshAttempt() - } -} - extension QuizViewController : UIWebViewDelegate { func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool { if let url = request.url { diff --git a/Stepic/SectionsViewController.swift b/Stepic/SectionsViewController.swift index 40e2964702..a33f3c5f7e 100644 --- a/Stepic/SectionsViewController.swift +++ b/Stepic/SectionsViewController.swift @@ -8,11 +8,11 @@ import UIKit import DownloadButton -import DZNEmptyDataSet -class SectionsViewController: UIViewController, ShareableController, UIViewControllerPreviewingDelegate { +class SectionsViewController: UIViewController, ShareableController, UIViewControllerPreviewingDelegate, ControllerWithStepikPlaceholder { + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() - @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var tableView: StepikTableView! let refreshControl = UIRefreshControl() var didRefresh = false @@ -29,6 +29,8 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr override func viewDidLoad() { super.viewDidLoad() + registerPlaceholder(placeholder: StepikPlaceholder(.noConnection), for: .connectionError) + LastStepGlobalContext.context.course = course self.navigationItem.title = course.title @@ -42,6 +44,8 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr self.navigationItem.rightBarButtonItems = [shareBarButtonItem, infoBarButtonItem] tableView.register(UINib(nibName: "SectionTableViewCell", bundle: nil), forCellReuseIdentifier: "SectionTableViewCell") + tableView.emptySetPlaceholder = StepikPlaceholder(.emptySections) + tableView.loadingPlaceholder = StepikPlaceholder(.emptySectionsLoading) refreshControl.addTarget(self, action: #selector(SectionsViewController.refreshSections), for: .valueChanged) if #available(iOS 10.0, *) { @@ -53,9 +57,6 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr refreshControl.beginRefreshing() refreshSections() - tableView.emptyDataSetDelegate = self - tableView.emptyDataSetSource = self - tableView.estimatedRowHeight = 44.0 tableView.rowHeight = UITableViewAutomaticDimension @@ -155,8 +156,15 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr var emptyDatasetState: EmptyDatasetState = .empty { didSet { - UIThread.performUI { - self.tableView.reloadEmptyDataSet() + switch emptyDatasetState { + case .refreshing: + isPlaceholderShown = false + tableView.showLoadingPlaceholder() + case .empty: + isPlaceholderShown = false + tableView.reloadData() + case .connectionError: + showPlaceholder(for: .connectionError) } } } @@ -467,78 +475,3 @@ extension SectionsViewController : PKDownloadButtonDelegate { } } } - -extension SectionsViewController : DZNEmptyDataSetSource { - - func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! { - switch emptyDatasetState { - case .empty: - return Images.emptyCoursesPlaceholder - case .connectionError: - return Images.noWifiImage.size250x250 - case .refreshing: - return Images.emptyCoursesPlaceholder - } - } - - func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - switch emptyDatasetState { - case .empty: - text = NSLocalizedString("EmptyTitle", comment: "") - break - case .connectionError: - text = NSLocalizedString("ConnectionErrorTitle", comment: "") - break - case .refreshing: - text = NSLocalizedString("Refreshing", comment: "") - break - } - - let attributes = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18.0), - NSAttributedStringKey.foregroundColor: UIColor.darkGray] - - return NSAttributedString(string: text, attributes: attributes) - } - - func description(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - - switch emptyDatasetState { - case .empty: - text = NSLocalizedString("PullToRefreshSectionsDescription", comment: "") - break - case .connectionError: - text = NSLocalizedString("PullToRefreshSectionsDescription", comment: "") - break - case .refreshing: - text = NSLocalizedString("RefreshingDescription", comment: "") - break - } - - let paragraph = NSMutableParagraphStyle() - paragraph.lineBreakMode = .byWordWrapping - paragraph.alignment = .center - - let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14.0), - NSAttributedStringKey.foregroundColor: UIColor.lightGray, - NSAttributedStringKey.paragraphStyle: paragraph] - - return NSAttributedString(string: text, attributes: attributes) - } - - func backgroundColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! { - return UIColor.white - } - - func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat { - // print("offset -> \((self.navigationController?.navigationBar.bounds.height) ?? 0 + UIApplication.sharedApplication().statusBarFrame.height)") - return 44 - } -} - -extension SectionsViewController : DZNEmptyDataSetDelegate { - func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool { - return true - } -} diff --git a/Stepic/Stepic-Bridging-Header.h b/Stepic/Stepic-Bridging-Header.h index a989530adc..43a49a0abb 100644 --- a/Stepic/Stepic-Bridging-Header.h +++ b/Stepic/Stepic-Bridging-Header.h @@ -8,6 +8,4 @@ #import -#import - #import "WKWebViewPanelManager.h" diff --git a/Stepic/StepikPlaceholder.swift b/Stepic/StepikPlaceholder.swift index 7c28696f61..328156ea11 100644 --- a/Stepic/StepikPlaceholder.swift +++ b/Stepic/StepikPlaceholder.swift @@ -11,14 +11,10 @@ import UIKit typealias StepikPlaceholderStyle = StepikPlaceholder.Style class StepikPlaceholder { - class var availablePlaceholders: [StepikPlaceholderStyle] { - return [Style.empty, Style.noConnection, Style.login, Style.emptyDownloads, Style.emptyNotifications, Style.emptySearch] - } - var style: StepikPlaceholderStyle var buttonAction: (() -> Void)? - init(_ style: StepikPlaceholderStyle, action: (() -> Void)?) { + init(_ style: StepikPlaceholderStyle, action: (() -> Void)? = nil) { self.style = style self.buttonAction = action } @@ -44,45 +40,3 @@ class StepikPlaceholder { } } } - -extension StepikPlaceholder.Style { - static let empty = StepikPlaceholderStyle(id: "empty", - image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), - text: NSLocalizedString("PlaceholderEmptyText", comment: ""), - buttonTitle: NSLocalizedString("PlaceholderEmptyButton", comment: "")) - static let noConnection = StepikPlaceholderStyle(id: "noConnection", - image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-noconnection"), scale: 0.35), - text: NSLocalizedString("PlaceholderNoConnectionText", comment: ""), - buttonTitle: NSLocalizedString("PlaceholderNoConnectionButton", comment: "")) - static let emptyDownloads = StepikPlaceholderStyle(id: "emptyDownloads", - image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-downloads"), scale: 0.46), - text: NSLocalizedString("PlaceholderEmptyDownloadsText", comment: ""), - buttonTitle: NSLocalizedString("PlaceholderEmptyDownloadsButton", comment: "")) - static let login = StepikPlaceholderStyle(id: "login", - image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-login"), scale: 0.59), - text: NSLocalizedString("PlaceholderLoginText", comment: ""), - buttonTitle: NSLocalizedString("PlaceholderLoginButton", comment: "")) - static let emptyNotifications = StepikPlaceholderStyle(id: "emptyNotifications", - image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-notifications"), scale: 0.48), - text: NSLocalizedString("PlaceholderEmptyNotificationsText", comment: ""), - buttonTitle: NSLocalizedString("PlaceholderEmptyNotificationsButton", comment: "")) - static let emptySearch = StepikPlaceholderStyle(id: "emptySearch", - image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-search"), scale: 0.49), - text: NSLocalizedString("PlaceholderEmptySearchText", comment: ""), - buttonTitle: NSLocalizedString("PlaceholderEmptySearchButton", comment: "")) -} - -class StepikPlaceholderContainer { - private var placeholder: StepikPlaceholder - - init(_ placeholder: StepikPlaceholder) { - self.placeholder = placeholder - } - - func build() -> StepikPlaceholderView { - let view = StepikPlaceholderView() - view.set(id: placeholder.style.id) - - return view - } -} diff --git a/Stepic/StepikPlaceholderStyle+Placeholders.swift b/Stepic/StepikPlaceholderStyle+Placeholders.swift new file mode 100644 index 0000000000..1a3744ebb0 --- /dev/null +++ b/Stepic/StepikPlaceholderStyle+Placeholders.swift @@ -0,0 +1,90 @@ +// +// StepikPlaceholderStyle+Placeholders.swift +// Stepic +// +// Created by Vladislav Kiryukhin on 20.03.2018. +// Copyright © 2018 Alex Karpov. All rights reserved. +// + +import Foundation + +extension StepikPlaceholder.Style { + static let empty = StepikPlaceholderStyle(id: "empty", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("PlaceholderEmptyText", comment: ""), + buttonTitle: NSLocalizedString("PlaceholderEmptyButton", comment: "")) + static let noConnection = StepikPlaceholderStyle(id: "noConnection", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-noconnection"), scale: 0.35), + text: NSLocalizedString("PlaceholderNoConnectionText", comment: ""), + buttonTitle: NSLocalizedString("PlaceholderNoConnectionButton", comment: "")) + static let emptyDownloads = StepikPlaceholderStyle(id: "emptyDownloads", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-downloads"), scale: 0.46), + text: NSLocalizedString("PlaceholderEmptyDownloadsText", comment: ""), + buttonTitle: NSLocalizedString("PlaceholderEmptyDownloadsButton", comment: "")) + static let login = StepikPlaceholderStyle(id: "login", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-login"), scale: 0.59), + text: NSLocalizedString("PlaceholderLoginText", comment: ""), + buttonTitle: NSLocalizedString("PlaceholderLoginButton", comment: "")) + static let emptyNotifications = StepikPlaceholderStyle(id: "emptyNotifications", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-notifications"), scale: 0.48), + text: NSLocalizedString("PlaceholderEmptyNotificationsText", comment: ""), + buttonTitle: NSLocalizedString("PlaceholderEmptyNotificationsButton", comment: "")) + static let emptyNotificationsLoading = StepikPlaceholderStyle(id: "emptyNotificationsLoading", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-notifications"), scale: 0.48), + text: NSLocalizedString("Refreshing", comment: ""), + buttonTitle: nil) + static let emptySearch = StepikPlaceholderStyle(id: "emptySearch", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-search"), scale: 0.49), + text: NSLocalizedString("PlaceholderEmptySearchText", comment: ""), + buttonTitle: NSLocalizedString("PlaceholderEmptySearchButton", comment: "")) + static let emptyCertificates = StepikPlaceholderStyle(id: "emptyCertificates", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("EmptyCertificatesTitle", comment: ""), + buttonTitle: NSLocalizedString("ChooseCourse", comment: "")) + static let emptyCertificatesLoading = StepikPlaceholderStyle(id: "emptyCertificatesLoading", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("Refreshing", comment: ""), + buttonTitle: nil) + static let emptyDiscussions = StepikPlaceholderStyle(id: "emptyDiscussions", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("NoDiscussionsTitle", comment: ""), + buttonTitle: nil) + static let emptyDiscussionsLoading = StepikPlaceholderStyle(id: "emptyDiscussionsLoading", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("Refreshing", comment: ""), + buttonTitle: nil) + static let emptyProfileLoading = StepikPlaceholderStyle(id: "emptyProfileLoading", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("Refreshing", comment: ""), + buttonTitle: nil) + static let emptySections = StepikPlaceholderStyle(id: "emptySections", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("EmptyTitle", comment: ""), + buttonTitle: nil) + static let emptySectionsLoading = StepikPlaceholderStyle(id: "emptySectionsLoading", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("Refreshing", comment: ""), + buttonTitle: nil) + static let emptyUnits = StepikPlaceholderStyle(id: "emptyUnits", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("PullToRefreshUnitsTitle", comment: ""), + buttonTitle: nil) + static let emptyUnitsLoading = StepikPlaceholderStyle(id: "emptyUnitsLoading", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("Refreshing", comment: ""), + buttonTitle: nil) + static let noConnectionQuiz = StepikPlaceholderStyle(id: "noConnectionQuiz", + image: nil, + text: NSLocalizedString("ConnectionErrorText", comment: ""), + buttonTitle: NSLocalizedString("TryAgain", comment: "")) + static let adaptiveCoursePassed = StepikPlaceholderStyle(id: "adaptiveCoursePassed", + image: PlaceholderImage(image: #imageLiteral(resourceName: "new-empty-empty"), scale: 0.99), + text: NSLocalizedString("NoRecommendations", comment: ""), + buttonTitle: nil) +} + +extension StepikPlaceholder.Style { + class var stepikStyledPlaceholders: [StepikPlaceholderStyle] { + return [StepikPlaceholderStyle.empty, StepikPlaceholderStyle.noConnection, StepikPlaceholderStyle.login, StepikPlaceholderStyle.emptyDownloads, StepikPlaceholderStyle.emptyNotifications, StepikPlaceholderStyle.emptySearch, StepikPlaceholderStyle.emptyCertificates, StepikPlaceholderStyle.emptyDiscussions, StepikPlaceholderStyle.emptyProfileLoading, StepikPlaceholderStyle.emptySections, StepikPlaceholderStyle.emptyUnits] + } +} diff --git a/Stepic/StepikPlaceholderView.swift b/Stepic/StepikPlaceholderView.swift index ad1fcf5a9e..11080d504a 100644 --- a/Stepic/StepikPlaceholderView.swift +++ b/Stepic/StepikPlaceholderView.swift @@ -34,7 +34,7 @@ class StepikPlaceholderView: NibInitializableView { lazy private var allPlaceholders: [StepikPlaceholderStyle.PlaceholderId: StepikPlaceholderStyle] = { var idToView: [StepikPlaceholderStyle.PlaceholderId: StepikPlaceholderStyle] = [:] - for placeholder in StepikPlaceholder.availablePlaceholders { + for placeholder in StepikPlaceholderStyle.stepikStyledPlaceholders { idToView[placeholder.id] = placeholder } return idToView @@ -48,6 +48,11 @@ class StepikPlaceholderView: NibInitializableView { return "StepikPlaceholderView" } + convenience init(placeholder: StepikPlaceholderStyle) { + self.init() + set(placeholder: placeholder) + } + @IBAction func onActionButtonClick(_ sender: Any) { delegate?.buttonDidClick(actionButton) } @@ -96,13 +101,11 @@ class StepikPlaceholderView: NibInitializableView { } if placeholder.buttonTitle != nil { - if !actionsStackView.arrangedSubviews.contains(actionButton) { - actionsStackView.insertArrangedSubview(actionButton, at: 1) - } + actionButton.alpha = 1.0 actionButton.isHidden = false } else { - actionsStackView.removeArrangedSubview(actionButton) - actionButton.isHidden = true + actionButton.alpha = 0.0 + actionButton.isHidden = !isVertical } if isVertical { @@ -123,17 +126,14 @@ class StepikPlaceholderView: NibInitializableView { } } - func set(id: StepikPlaceholderStyle.PlaceholderId) { - guard let placeholderWithId = allPlaceholders[id] else { - return - } + func set(placeholder: StepikPlaceholderStyle) { + currentPlaceholder = placeholder - currentPlaceholder = placeholderWithId + imageView.image = placeholder.image?.image - imageView.image = placeholderWithId.image?.image - textLabel.text = placeholderWithId.text - actionButton.setTitle(placeholderWithId.buttonTitle, for: .normal) + textLabel.text = placeholder.text + actionButton.setTitle(placeholder.buttonTitle, for: .normal) - rebuildConstraints(for: placeholderWithId) + rebuildConstraints(for: placeholder) } } diff --git a/Stepic/StepikTableView.swift b/Stepic/StepikTableView.swift index 966e79d6cb..89f3e3623e 100644 --- a/Stepic/StepikTableView.swift +++ b/Stepic/StepikTableView.swift @@ -11,18 +11,16 @@ import UIKit class StepikTableView: UITableView { // Empty state placeholder - var emptySetPlaceholder: StepikPlaceholder? { - didSet { - if let p = emptySetPlaceholder { - emptySetView?.removeFromSuperview() - emptySetView = StepikPlaceholderContainer(p).build() - (emptySetView as? StepikPlaceholderView)?.delegate = self - } - } - } + var emptySetPlaceholder: StepikPlaceholder? - // View for empty state - private var emptySetView: UIView? + // Loading state placeholder + var loadingPlaceholder: StepikPlaceholder? + + // View for placeholders + lazy private var placeholderView: StepikPlaceholderView = { + let view = StepikPlaceholderView() + return view + }() // Trick with removing cell separators: we should store previous footer to restore private var savedFooterView: UIView? @@ -33,48 +31,59 @@ extension StepikTableView { return (0.. 0 } - private func handleEmptySetView(isHidden: Bool) { + private func handlePlaceholder(isHidden: Bool) { if isHidden { tableFooterView = savedFooterView - emptySetView?.isHidden = true + placeholderView.isHidden = true return } - updateEmptySetLayout() + updatePlaceholderLayout() // Remove cell separators savedFooterView = self.tableFooterView tableFooterView = UIView() + placeholderView.isHidden = false + } - emptySetView?.isHidden = false + private func handleEmptySetPlaceholder(isHidden: Bool) { + if let p = emptySetPlaceholder, !isHidden { + placeholderView.set(placeholder: p.style) + placeholderView.delegate = self + } + handlePlaceholder(isHidden: isHidden) } - private func updateEmptySetLayout() { - guard let emptySetView = emptySetView else { - return + func showLoadingPlaceholder(force: Bool = false) { + if let p = loadingPlaceholder { + placeholderView.set(placeholder: p.style) + placeholderView.delegate = self } + handlePlaceholder(isHidden: !force && hasContent) + } - if emptySetView.superview == nil { - emptySetView.translatesAutoresizingMaskIntoConstraints = false + private func updatePlaceholderLayout() { + if placeholderView.superview == nil { + placeholderView.translatesAutoresizingMaskIntoConstraints = false - addSubview(emptySetView) - emptySetView.alignCenter(withView: self) - emptySetView.align(toView: self) + addSubview(placeholderView) + placeholderView.alignCenter(withView: self) + placeholderView.align(toView: self) - emptySetView.setNeedsLayout() - emptySetView.layoutIfNeeded() + placeholderView.setNeedsLayout() + placeholderView.layoutIfNeeded() } - bringSubview(toFront: emptySetView) + bringSubview(toFront: placeholderView) } override func reloadData() { super.reloadData() - handleEmptySetView(isHidden: hasContent) + handleEmptySetPlaceholder(isHidden: hasContent) } override func endUpdates() { super.endUpdates() - handleEmptySetView(isHidden: hasContent) + handleEmptySetPlaceholder(isHidden: hasContent) } } diff --git a/Stepic/Styles.swift b/Stepic/Styles.swift deleted file mode 100644 index a5ccd503f3..0000000000 --- a/Stepic/Styles.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// Styles.swift -// OstrenkiyPlaceholderView -// -// Created by Alexander Karpov on 08.02.16. -// Copyright © 2016 Alex Karpov. All rights reserved. -// - -import UIKit - -struct PlaceholderStyle { - struct LabelStyle { - var font: UIFont = UIFont.systemFont(ofSize: 14) - var textColor: UIColor = UIColor.lightGray - var textAlignment: NSTextAlignment = NSTextAlignment.center - var lineBreakMode: NSLineBreakMode = NSLineBreakMode.byWordWrapping - } - - struct ButtonStyle { - var font: UIFont = UIFont.systemFont(ofSize: 17) - var borderType: BorderType = .none - var borderColor: UIColor = UIColor.clear - var backgroundColor: UIColor = UIColor.clear - var textColor: UIColor = UIColor.blue - } - - var title = LabelStyle() - var description = LabelStyle() - var button = ButtonStyle() -} - -var stepicPlaceholderStyle: PlaceholderStyle { - var style = PlaceholderStyle() - style.title.font = UIFont.boldSystemFont(ofSize: 18) - style.button.borderType = .none - style.button.borderColor = UIColor.mainDark - style.button.backgroundColor = UIColor.white - style.button.textColor = UIColor.mainDark - return style -} - -enum BorderType { - case none, rounded, rect -} diff --git a/Stepic/UnitsViewController.swift b/Stepic/UnitsViewController.swift index 24adeb2827..79c160d34a 100644 --- a/Stepic/UnitsViewController.swift +++ b/Stepic/UnitsViewController.swift @@ -8,11 +8,11 @@ import UIKit import DownloadButton -import DZNEmptyDataSet -class UnitsViewController: UIViewController, ShareableController, UIViewControllerPreviewingDelegate { +class UnitsViewController: UIViewController, ShareableController, UIViewControllerPreviewingDelegate, ControllerWithStepikPlaceholder { + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() - @IBOutlet weak var tableView: UITableView! + @IBOutlet weak var tableView: StepikTableView! /* There are 2 ways of instantiating the controller @@ -36,6 +36,8 @@ class UnitsViewController: UIViewController, ShareableController, UIViewControll override func viewDidLoad() { super.viewDidLoad() + registerPlaceholder(placeholder: StepikPlaceholder(.noConnection), for: .connectionError) + updateTitle() self.navigationItem.backBarButtonItem?.title = " " @@ -54,8 +56,9 @@ class UnitsViewController: UIViewController, ShareableController, UIViewControll } refreshControl.layoutIfNeeded() - tableView.emptyDataSetDelegate = self - tableView.emptyDataSetSource = self + tableView.emptySetPlaceholder = StepikPlaceholder(.emptyUnits) + tableView.loadingPlaceholder = StepikPlaceholder(.emptyUnitsLoading) + refreshControl.beginRefreshing() refreshUnits() @@ -208,8 +211,15 @@ class UnitsViewController: UIViewController, ShareableController, UIViewControll var emptyDatasetState: EmptyDatasetState = .empty { didSet { - UIThread.performUI { - self.tableView.reloadEmptyDataSet() + switch emptyDatasetState { + case .refreshing: + isPlaceholderShown = false + tableView.showLoadingPlaceholder() + case .empty: + isPlaceholderShown = false + tableView.reloadData() + case .connectionError: + showPlaceholder(for: .connectionError) } } } @@ -663,78 +673,3 @@ extension UnitsViewController : PKDownloadButtonDelegate { } } } - -extension UnitsViewController : DZNEmptyDataSetSource { - - func image(forEmptyDataSet scrollView: UIScrollView!) -> UIImage! { - switch emptyDatasetState { - case .empty: - return Images.emptyCoursesPlaceholder - case .connectionError: - return Images.noWifiImage.size250x250 - case .refreshing: - return Images.emptyCoursesPlaceholder - } - } - - func title(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - switch emptyDatasetState { - case .empty: - text = NSLocalizedString("PullToRefreshUnitsTitle", comment: "") - break - case .connectionError: - text = NSLocalizedString("ConnectionErrorTitle", comment: "") - break - case .refreshing: - text = NSLocalizedString("Refreshing", comment: "") - break - } - - let attributes = [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 18.0), - NSAttributedStringKey.foregroundColor: UIColor.darkGray] - - return NSAttributedString(string: text, attributes: attributes) - } - - func description(forEmptyDataSet scrollView: UIScrollView!) -> NSAttributedString! { - var text: String = "" - - switch emptyDatasetState { - case .empty: - text = NSLocalizedString("PullToRefreshUnitsDescription", comment: "") - break - case .connectionError: - text = NSLocalizedString("PullToRefreshUnitsDescription", comment: "") - break - case .refreshing: - text = NSLocalizedString("RefreshingDescription", comment: "") - break - } - - let paragraph = NSMutableParagraphStyle() - paragraph.lineBreakMode = .byWordWrapping - paragraph.alignment = .center - - let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 14.0), - NSAttributedStringKey.foregroundColor: UIColor.lightGray, - NSAttributedStringKey.paragraphStyle: paragraph] - - return NSAttributedString(string: text, attributes: attributes) - } - - func backgroundColor(forEmptyDataSet scrollView: UIScrollView!) -> UIColor! { - return UIColor.white - } - - func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat { - // print("offset -> \((self.navigationController?.navigationBar.bounds.height) ?? 0 + UIApplication.sharedApplication().statusBarFrame.height)") - return 44 - } -} - -extension UnitsViewController : DZNEmptyDataSetDelegate { - func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool { - return true - } -} diff --git a/StepicAdaptiveCourse/AdaptiveCardsStepsViewController.swift b/StepicAdaptiveCourse/AdaptiveCardsStepsViewController.swift index 00292e3f60..8bc63eae11 100644 --- a/StepicAdaptiveCourse/AdaptiveCardsStepsViewController.swift +++ b/StepicAdaptiveCourse/AdaptiveCardsStepsViewController.swift @@ -8,6 +8,13 @@ import UIKit +extension StepikPlaceholder.Style { + static let adaptiveCoursePassedAdaptive = StepikPlaceholderStyle(id: "adaptiveCoursePassedAdaptive", + image: nil, + text: NSLocalizedString("NoRecommendations", comment: ""), + buttonTitle: nil) +} + class AdaptiveCardsStepsViewController: CardsStepsViewController { @IBOutlet weak var levelProgress: RatingProgressView! @IBOutlet weak var tapProxyView: TapProxyView! @@ -48,6 +55,8 @@ class AdaptiveCardsStepsViewController: CardsStepsViewController { override func viewDidLoad() { super.viewDidLoad() + registerPlaceholder(placeholder: StepikPlaceholder(.adaptiveCoursePassedAdaptive), for: .adaptiveCoursePassed) + tapProxyView.targetView = trophyButton tapBackProxyView.targetView = backButton trophyButton.tintColor = UIColor.mainDark diff --git a/StepicAdaptiveCourse/AdaptiveCourseSelectViewController.swift b/StepicAdaptiveCourse/AdaptiveCourseSelectViewController.swift index de740300ee..3f1c0e7dd2 100644 --- a/StepicAdaptiveCourse/AdaptiveCourseSelectViewController.swift +++ b/StepicAdaptiveCourse/AdaptiveCourseSelectViewController.swift @@ -12,7 +12,15 @@ enum AdaptiveCourseSelectViewState { case loading, normal, error } -class AdaptiveCourseSelectViewController: UIViewController, AdaptiveCourseSelectView { +extension StepikPlaceholder.Style { + static let noConnectionAdaptive = StepikPlaceholderStyle(id: "noConnectionAdaptive", + image: nil, + text: NSLocalizedString("PlaceholderNoConnectionText", comment: ""), + buttonTitle: NSLocalizedString("PlaceholderNoConnectionButton", comment: "")) +} + +class AdaptiveCourseSelectViewController: UIViewController, AdaptiveCourseSelectView, ControllerWithStepikPlaceholder { + var placeholderContainer: StepikPlaceholderControllerContainer = StepikPlaceholderControllerContainer() @IBOutlet weak var tableView: UITableView! @IBOutlet weak var loadingContainerView: UIView! @@ -21,32 +29,20 @@ class AdaptiveCourseSelectViewController: UIViewController, AdaptiveCourseSelect var data: [AdaptiveCourseSelectViewData] = [] var didControllerDisplayBefore = false - lazy var placeholderView: PlaceholderView = { - let v = PlaceholderView() - self.view.insertSubview(v, aboveSubview: self.view) - v.align(toView: self.view) - v.delegate = self - v.backgroundColor = self.view.backgroundColor - return v - }() - var presenter: AdaptiveCourseSelectPresenter? var state: AdaptiveCourseSelectViewState = .normal { didSet { switch state { case .normal: - self.placeholderView.isHidden = true + isPlaceholderShown = false self.tableView.isHidden = false self.loadingContainerView.isHidden = true case .error: - self.placeholderView.isHidden = false + showPlaceholder(for: .connectionError) self.tableView.isHidden = true self.loadingContainerView.isHidden = true - - // Refresh placeholder state - self.placeholderView.datasource = self case .loading: - self.placeholderView.isHidden = true + isPlaceholderShown = false self.tableView.isHidden = true self.loadingContainerView.isHidden = false } @@ -56,6 +52,10 @@ class AdaptiveCourseSelectViewController: UIViewController, AdaptiveCourseSelect override func viewDidLoad() { super.viewDidLoad() + registerPlaceholder(placeholder: StepikPlaceholder(.noConnectionAdaptive, action: { [weak self] in + self?.presenter?.tryAgain() + }), for: .connectionError) + loadingLabel.text = NSLocalizedString("AdaptiveCourseSelectLoading", comment: "") title = NSLocalizedString("AdaptiveCourseSelectTitle", comment: "") @@ -130,53 +130,3 @@ extension AdaptiveCourseSelectViewController: AdaptiveCourseTableViewCellDelegat tableView(tableView, didSelectRowAt: indexPath) } } - -extension AdaptiveCourseSelectViewController: PlaceholderViewDataSource { - func placeholderImage() -> UIImage? { - switch state { - case .error: - return Images.placeholders.connectionError - default: - return nil - } - } - - func placeholderButtonTitle() -> String? { - switch state { - case .error: - return NSLocalizedString("TryAgain", comment: "") - default: - return nil - } - } - - func placeholderDescription() -> String? { - switch state { - case .error: - return nil - default: - return nil - } - } - - func placeholderStyle() -> PlaceholderStyle { - var style = PlaceholderStyle() - style.button.textColor = UIColor.mainDark - return style - } - - func placeholderTitle() -> String? { - switch state { - case .error: - return NSLocalizedString("ConnectionErrorText", comment: "") - default: - return nil - } - } -} - -extension AdaptiveCourseSelectViewController: PlaceholderViewDelegate { - func placeholderButtonDidPress() { - presenter?.tryAgain() - } -} From 048c5edd28b4a37cc1e4a24db5db64d8dd453e83 Mon Sep 17 00:00:00 2001 From: Alexander Karpov Date: Wed, 4 Apr 2018 11:40:18 +0300 Subject: [PATCH 09/14] Set version to 1.56 --- Sb/SberbankUniversity-Info.plist | Bin 2294 -> 2294 bytes Stepic.xcodeproj/project.pbxproj | 4 ++-- Stepic/Info.plist | 4 ++-- .../Content/1838/AdaptiveInfo.plist | 4 ++-- .../Content/3124/AdaptiveInfo.plist | 4 ++-- .../Content/3149/AdaptiveInfo.plist | 4 ++-- .../Content/3150/AdaptiveInfo.plist | 4 ++-- StepicTests/Info.plist | 4 ++-- StepicWatch Extension/Info.plist | 4 ++-- StepicWatch/Info.plist | 4 ++-- StepikTV/Info.plist | 4 ++-- StepikTVTests/Info.plist | 4 ++-- StickerPackExtension/Info.plist | 4 ++-- UITests/Screenshots/Info.plist | 4 ++-- 14 files changed, 26 insertions(+), 26 deletions(-) diff --git a/Sb/SberbankUniversity-Info.plist b/Sb/SberbankUniversity-Info.plist index afe43227c1b9e86bff63bb7cc1799459698a4cf8..bbe6585696dbe39484a66875ac69ee64217735d1 100644 GIT binary patch literal 2294 zcmVTeiyE~kq!dQNERnq=XYs#DI19sQR=O_2(f@z4X_*lArHqy{4uKvAe>bZ@uRpiVPEy;J%oD@2a1-BZ+cv9^(I8T&O*-tsJgBwvP!St zUg7`gYU$+qMrO(*F}|r2U|+O`v3*l^Hi zMDZFIB5J=II!8pvA4(h}=&?aFO3=b{o5icG_bl`3*JXw)rWYL`1AAdU5X=H8?5DTW zjCiE>HcE8eW}zzvjbinO3+2@DnFl%gxQaR&#y|!9uY?g}D~3aVvOh$d-Y-^eHbJNi zO%+8;=R)&V9TnYYB3iHDhSdEfJuyxTM-;gqU39cGc@uP)$$60WeB&Ip{hLbkc7r}< z;=t8Mkoj>Omzro`$5f?dwpcjyeHeT#%a54&QzX#RNPZrtrg_CS*VWUsm0fLuhO|!f zn@CF1ZPOHPGfNyK2M`@_7sF*ts*M2~cyXvg{h5f&=S&grr`6KIV}z=IpHi@e2c^XM z62Jd*dC)`$`h)?vykMY3#F?D51Il#tx4Qjzb2CU_#;KOl?k7Bh0Uf2VSoH-45-)#i zMg(aj8a|K}&c4E-W>F<`EWqKA+|lOTfk!Uo!WYkTJD*J&Lr;_c>sz8UDP6ov=L?0cmYbbMtDkb-Zo%YvGINq1c7qs(;RlRl_xqSl*%$`JO~;Q1(B_&EJjok6 z4I|~dq*A)raEI7}1|Z)XPBPV1&?o2Cf6dzPHt)n9Yn!w#zBG}Xg8^gWOfs0qf>-4A z7~x}=shj~xhvs*we$vJj7`vtO!{*9>)qsBZXMd}oOgN~GQ=9GN0}6riuH;G!G8x4? z9CxEFXq+m^$uXnCB`9f5H_|0Is^JkOqC2Rs|f?oJTNhGFe3ag9PMx28(E6F=Y}&5ZqJ z8Z!;U9)p*DqpjzwTy7n@9dc$Rh@gH;Q|Byb8+}2b%H}^Q)O;=SR!}Rz&@b3Y@}o(7 z%HW|LMWj)n;BYOBzhZ9C+^S;`Y~!l;nn_0Xx8+VOKBLVm?f^IwU7)0qSx9|mGEuXL z^0_=|LqmT<^-{DEe{c{+``(+HeB^R1l)tNV-MKdLkgT}0p1+Q9cMRJt?UbamlZ>W) z(99!=Nd~L-9H*KK9t!=G&H&T2{G)@ad`vmW1>!#do}pE3&>*9$8&T+%_u9W}j30}Y zt1_&F;nhxiSV@N_MNp+I8sohmuwq|6!eQq1C@}agc~WVHJcqb6NGMgi zXs7qT8<#7yiM!byxBp`DR$j7nsooYpXOj7hL#h0dKk3@g^kvWq%$zkW3%6uIqFA?I z4*UdT<`n@_lUIu+=j)^*f@F*AB{KL4GF_)iY&9OS2k%NwFe}KtCe7P&gczT=?+#$r z^)}iW7)E+Qr+Pl(Er0>szK`->5oQLyHx6DZk0s=Q_d&WFsctXIIDQaC;r}B=*?NHku^kg~IfQ_+foM?_ zGvZmiq&VvTJ}wHhhup^~_>+4*N!Q#w?V@|;Qu9~F-sJ37JsNrFPW4K$`)Lz4vDqH~ zSVYF_T+NnrasX!nE-oU>!*t24*_-5AH?8|5T^~3Ce(7aw z*aVLMS*l56_X#H#7ggVVvEO>X@Rqna%P24Fi07)W-64iANx5A?J7wBZaU@RIx0l)A z{7!5k~rx1fsPnaDO|!6^DjP!UM~0u`ulGrxDV7h#U8!tVrd~K?*HXEKJ5U zEuBwu7*^P?&pVaW-fdY(b@WJAJ@uctlt&290Fq7Jpzb}gQ}DH*tYDBK=^$kujN2{x zY!b|FVuJApigX literal 2294 zcmVm`nzev@-;mIUqyv%2clb4Dxc z&hNZaXyi}7V>~FY=$2XhUfo{ zy8}*WoIAEeCu~#9pl795W4|I99AFs%`ZT8*n74gBY^QO{bn9pn*_z-A#qx`uI{Bh_+(nYKck|X~n5uvuR{cY1Z`ysUGp>K|c2Y zc#;V7P=g$BwS}<}y)RR+M5iuf_o!lz`BNWIuRc}P<6n}F2#-_7uly#zCPV`@Qs-ly zdb@iKN?YuhAJa~8At{P(J7(cCJ14BvAx~#XPb__*eue|;6{OHs*mbz7+oaAOlqY#f z@xK!gO128(HyHj^OWtgF=s1{zuKF+4H;#swcpG9_myF6SAt;DV6ZQo&fH8K@1x528 z$HcuRqWt%0xR<hg!l^~^vFfL7{Lzs_|{vmf+p0EB^hQ z*kJVnRQubM;O^fv>O>9$;gAjZEuuol6{vS3%Pc6=AuP)Vv#75bS6TxpOTX(ChWH@c zU;YbdJeq^p4yeNKwD=tn>ib`~LT=p7F*_*pmjoA~|p(m%{_w$l@9#&8JXbbE1FIwzWvm9cb^DiP2yv z=EVih5I*5aP`=k{YfjKD?}6|f8T6xy{MqB`e&Dcf|DR2zV7llc_Uu7KBM$nZvl{xB z!edBCS+Kk4!^IvuH%v0p31L6ynE33Be zVPlbR>ki$XYI&q|!NYP>T8X&+pi}62GiU-8S64e)Ps$Es*wfk!l~7;AYxE#W(D%x< zontg&lCgpDL6R^Go3qUS;}J|gG(AeLQSdopgVOv%=3jPryCNM<5WU68jf-=A;&f$> z9NvB}^84zsyd@vwNV^h46{@S2b$$6kk5O0xrKx@rJOS7L;x+bGv!Al_26W{=vY58~ z*b9K(fS0~`>D!+x8|R;Kxn*Z43PAhj+iZ1&^-tVC`;6yomOfer%E^Pf9PS@ZM|wCQu0IkoU?Fn4 zNngxppNBifO3+KAx6r)i74jFMJQij8WBEa|X>z1+boMPzj^(_&>sBZ|o_K*!J)t6V>Dl`QJ$? zZl_mF|Gi4MV|zQMyGQAprYEoU#b^Zmu*b2nm0tn*bN_c@Hrx5WYWW7FW_|=c(|5=q z`!o4pPa`cVYhSGARKV=nb=$wpsV=d2vvZ2;`K}TZXa9t4>mB-Ae@)v^tf>L00+=H^D;{7@~qUfe@{XLS)MdIjZVVC{0d>S^zAhknsDl?xr8K5#dnd zf4&0rBqDTP27kAv1A_%I^rF&NVO-v4Pov^4fVC&iY;4x;%LU1jBZT^MT9pw4!mJJo zdR?PsX;juCFqjeR>vzilxfe4%9SJPp0WA2SiJLWJ=bVfkV(;BT1Inw)O$lFDLQ4;& zmMR3v_49_5mj;ZUG*(?P|C^WqlzIybKma%IRx@-s{;O09jK+UEov$8nfi0b6Td#oo z^3y5|yyZAOfGf|v3#*@-=14=uJU#4uNu7CFBundlePackageType APPL CFBundleShortVersionString - 1.55 + 1.56 CFBundleSignature ???? CFBundleURLTypes @@ -52,7 +52,7 @@ CFBundleVersion - 82 + 83 Fabric APIKey diff --git a/StepicAdaptiveCourse/Content/1838/AdaptiveInfo.plist b/StepicAdaptiveCourse/Content/1838/AdaptiveInfo.plist index ba9ebd8c36..fc8320847d 100644 --- a/StepicAdaptiveCourse/Content/1838/AdaptiveInfo.plist +++ b/StepicAdaptiveCourse/Content/1838/AdaptiveInfo.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.55 + 1.56 CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 82 + 83 Fabric APIKey diff --git a/StepicAdaptiveCourse/Content/3124/AdaptiveInfo.plist b/StepicAdaptiveCourse/Content/3124/AdaptiveInfo.plist index 393d8fe71f..75b116f687 100644 --- a/StepicAdaptiveCourse/Content/3124/AdaptiveInfo.plist +++ b/StepicAdaptiveCourse/Content/3124/AdaptiveInfo.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.55 + 1.56 CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 82 + 83 Fabric APIKey diff --git a/StepicAdaptiveCourse/Content/3149/AdaptiveInfo.plist b/StepicAdaptiveCourse/Content/3149/AdaptiveInfo.plist index 2e943ad49e..88a6a3442e 100644 --- a/StepicAdaptiveCourse/Content/3149/AdaptiveInfo.plist +++ b/StepicAdaptiveCourse/Content/3149/AdaptiveInfo.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.55 + 1.56 CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 82 + 83 Fabric APIKey diff --git a/StepicAdaptiveCourse/Content/3150/AdaptiveInfo.plist b/StepicAdaptiveCourse/Content/3150/AdaptiveInfo.plist index 067a4e645b..db1badd53a 100644 --- a/StepicAdaptiveCourse/Content/3150/AdaptiveInfo.plist +++ b/StepicAdaptiveCourse/Content/3150/AdaptiveInfo.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.55 + 1.56 CFBundleURLTypes @@ -44,7 +44,7 @@ CFBundleVersion - 82 + 83 Fabric APIKey diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index 11b81595ee..c7624a5fd5 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.55 + 1.56 CFBundleSignature ???? CFBundleVersion - 82 + 83 diff --git a/StepicWatch Extension/Info.plist b/StepicWatch Extension/Info.plist index 7dcb590bf2..4b864e9895 100644 --- a/StepicWatch Extension/Info.plist +++ b/StepicWatch Extension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.55 + 1.56 CFBundleVersion - 82 + 83 CLKComplicationPrincipalClass $(PRODUCT_MODULE_NAME).ComplicationController CLKComplicationSupportedFamilies diff --git a/StepicWatch/Info.plist b/StepicWatch/Info.plist index 76efc4d105..7735c7f819 100644 --- a/StepicWatch/Info.plist +++ b/StepicWatch/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.55 + 1.56 CFBundleVersion - 82 + 83 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/StepikTV/Info.plist b/StepikTV/Info.plist index 56cbeca775..328f95b17f 100644 --- a/StepikTV/Info.plist +++ b/StepikTV/Info.plist @@ -15,9 +15,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.55 + 1.56 CFBundleVersion - 82 + 83 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/StepikTVTests/Info.plist b/StepikTVTests/Info.plist index 0bf4963f83..aead604be0 100644 --- a/StepikTVTests/Info.plist +++ b/StepikTVTests/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.55 + 1.56 CFBundleVersion - 82 + 83 diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index bf448625bc..7e9e5fa21c 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 1.55 + 1.56 CFBundleVersion - 82 + 83 NSExtension NSExtensionPointIdentifier diff --git a/UITests/Screenshots/Info.plist b/UITests/Screenshots/Info.plist index 0e02b89d9e..aba3dc31e1 100644 --- a/UITests/Screenshots/Info.plist +++ b/UITests/Screenshots/Info.plist @@ -15,8 +15,8 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.55 + 1.56 CFBundleVersion - 82 + 83 From aea0f50ded563569e1f70608e9d1065d9d1fd169 Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Wed, 4 Apr 2018 18:22:01 +0300 Subject: [PATCH 10/14] Fix python autocomplete words --- Stepic/autocomplete_suggestions.plist | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Stepic/autocomplete_suggestions.plist b/Stepic/autocomplete_suggestions.plist index 36fc3ae73f..b2a3120204 100644 --- a/Stepic/autocomplete_suggestions.plist +++ b/Stepic/autocomplete_suggestions.plist @@ -2,6 +2,43 @@ + python + + False + class + finally + is + return + None + continue + for + lambda + try + True + def + from + nonlocal + while + and + del + global + not + with + as + elif + if + or + yield + assert + else + import + pass + print + break + except + in + raise + cpp bool From 7e7e98e2633bf7f091833637b758184d9b33c23f Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Wed, 4 Apr 2018 18:28:34 +0300 Subject: [PATCH 11/14] Removed share button in profile for anonymous (#266) --- Stepic/ProfileViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Stepic/ProfileViewController.swift b/Stepic/ProfileViewController.swift index 20319b4aa9..5553e26690 100644 --- a/Stepic/ProfileViewController.swift +++ b/Stepic/ProfileViewController.swift @@ -21,10 +21,14 @@ class ProfileViewController: MenuViewController, ProfileView, ControllerWithStep case .refreshing: showPlaceholder(for: .refreshing) case .anonymous: + navigationItem.rightBarButtonItem = nil showPlaceholder(for: .anonymous) case .error: showPlaceholder(for: .connectionError) case .authorized: + if let button = shareBarButtonItem { + navigationItem.rightBarButtonItem = button + } isPlaceholderShown = false } } From 3da986d56b3ec81f0e7bb8c71a4ef5a549eaa89b Mon Sep 17 00:00:00 2001 From: Alexander Karpov Date: Wed, 4 Apr 2018 18:31:18 +0300 Subject: [PATCH 12/14] increment build --- Sb/SberbankUniversity-Info.plist | Bin 2294 -> 2294 bytes Stepic.xcodeproj/project.pbxproj | 4 ++-- Stepic/Info.plist | 2 +- .../Content/1838/AdaptiveInfo.plist | 2 +- .../Content/3124/AdaptiveInfo.plist | 2 +- .../Content/3149/AdaptiveInfo.plist | 2 +- .../Content/3150/AdaptiveInfo.plist | 2 +- StepicTests/Info.plist | 2 +- StepicWatch Extension/Info.plist | 2 +- StepicWatch/Info.plist | 2 +- StepikTV/Info.plist | 2 +- StepikTVTests/Info.plist | 2 +- StickerPackExtension/Info.plist | 2 +- UITests/Screenshots/Info.plist | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Sb/SberbankUniversity-Info.plist b/Sb/SberbankUniversity-Info.plist index bbe6585696dbe39484a66875ac69ee64217735d1..6bd8349bce5fec2f3fcef374f5cda4a4912240b7 100644 GIT binary patch literal 2294 zcmV+U~pmc2cSeCORb{C78t~A8Cr+Ez{TZ)z%#9k`@r_m z)=&$g#`{PJ-i;Mnz&t?kT=n5g;aJ{pVt?o@Z`_RwJJ(0p2;*bxVA|G>OJ>wxaE0Kx z>azg;(T>h6o-5VXgEHOEUQlFwn@(av5?$?;@(UEFx<_u1^HNRoBS22*2%&L>LN0Z;_DAM>~PDJ2a%Ke7Oiw@$%sa!MiuP zUuXM#UOhQ4f}$y|^6Lgcj#e8gV*oa9K_jx^Rc6UUb0Jop=sMbNwm23`_I*qg(d5bmP%kfn# z|3X%1u>Kt=ga|K8cO`N0>=ts{?n0_YzvZ_4X$R0-`JzT*0j!O+eR_OaHpT{)C+gndVmTmvV{pXt>$QXYYoit8<*x8+e%fPUFs;XkoSVZsL_r9XfLq1#m`qk?iD z=Aga^tW_m4$VW!Uv$(cCw1T4;733jU6TwIrHQ%nz@K+Jp(qLJJiI@?w_;&x3=y+PF zbcZB&vSv2}pNH`!fJ?cU!>dkwi0udt#4eyroWww}L~QFplq0@u@VKHUBhG*+ z&@6k+GIJkNdaTSdSUwe@EN+t@hQ7Q#Vj1&O)X+Tx>*xNqlP1F&iiU}`PJ7TbU+=C7 zF5CBRW~!7P8%cU$vhjjws^k{vMO2WndFwfdI}nz51f)Apv#mb7C~dkyW*Amji3?R% zo$;o8gkDD3R#l}by?QIDFsP6$orZJ^toK4cw&a3|3`kvXXbNS~KiypX6V8!Il_HPJaF6y*%O`_L-> zi*Ea&m_A$Q*B?EwB>bg*ToI}>eTDem7>1mGLkZ$ep}!^bW;99xs}klJf@_e|45WWq zZX2kz?Z8DvbyrZKi&fn-_as~4J0?hl5hYT7LTA3y9DFVvNQ3U-ua~|9xU{mk_x#yq z+W7W78~BZ|Hs|4m{q(D9+(tpw1(UbD0lG0N;prRy3Uh%WEGd*z%umUnb1*vEf3`k% z$cX`5-r}#0v$WWZKsgA?vO$Q8gm7s68~)|&pefA`?_E(MXCJ$%Rg~hKI2w!97~H1r zZ11tM7@+}RM`s3~Gy=6YBu4f6+AnW-7Iri&9NGxc;$Jj}lW-bbJW^0PTd zGs%O64kyt;kw?s!0L)}l^6xBje`bTSF>|Klp7*`C)c#6K# z24MH1VB25@AAoJ?8#UFeA7@{3&`OROnMrE@4-K2i{_>Q`=A(aFsEczFqzn(jr5!7% zWDcH4%&LVjAAWx6%O&4p(U!b6YOU_F94zL&X*h;iIyYd$TiSHMR%43b8zF-C>`cvS z)qwF|ODX`HM->xHvyQ}L?%mKh6R~QfiP2QbZT#wT+N6>Vu6eqf;>0UMh5Xe-Bm8mz zl0&{~aj@=NVOuW3Qu!jpr7pcbeFmd)7!}ByH& zTLxBc=NFFtdWjm%mg)`H`1tpNZx^)6#5;#BhqB>0gxrXHx_}K7ovcd6<_Hy1Fejn7 z3eaXT{E!-UQyGzY8?g*gjd5}9q2&t1P?ES%2|ZB|mLEpt?XJcPeZO~qe_XDTt7VS} zMI!;!h|9wM$YVOfd*J#8EUZ~CE!`vt86YRk(ASAO4yEX>dr^}OLy?ooXFUpF)9=+y zrP8W7;2vpFLToLX^A%^;Gu{Z5)kVTOze22ltfveKHwzjWJR$;W4)36>PfyNj6s&(z z9fTiF|MG2c$1p9;$(%_ceU|k1fIGzzn&91mmLp3x1c`Ui%FlF2`j+XMM~i*l_EU#c z8Qld;jDTahX!iDk`pKMEoK`sVjAFzD?B&LY7=6(pgVef*AOEEV1e?)(r(*cx!| Q`q_QhM)abqS66SqBLvigk^lez literal 2294 zcmVTeiyE~kq!dQNERnq=XYs#DI19sQR=O_2(f@z4X_*lArHqy{4uKvAe>bZ@uRpiVPEy;J%oD@2a1-BZ+cv9^(I8T&O*-tsJgBwvP!St zUg7`gYU$+qMrO(*F}|r2U|+O`v3*l^Hi zMDZFIB5J=II!8pvA4(h}=&?aFO3=b{o5icG_bl`3*JXw)rWYL`1AAdU5X=H8?5DTW zjCiE>HcE8eW}zzvjbinO3+2@DnFl%gxQaR&#y|!9uY?g}D~3aVvOh$d-Y-^eHbJNi zO%+8;=R)&V9TnYYB3iHDhSdEfJuyxTM-;gqU39cGc@uP)$$60WeB&Ip{hLbkc7r}< z;=t8Mkoj>Omzro`$5f?dwpcjyeHeT#%a54&QzX#RNPZrtrg_CS*VWUsm0fLuhO|!f zn@CF1ZPOHPGfNyK2M`@_7sF*ts*M2~cyXvg{h5f&=S&grr`6KIV}z=IpHi@e2c^XM z62Jd*dC)`$`h)?vykMY3#F?D51Il#tx4Qjzb2CU_#;KOl?k7Bh0Uf2VSoH-45-)#i zMg(aj8a|K}&c4E-W>F<`EWqKA+|lOTfk!Uo!WYkTJD*J&Lr;_c>sz8UDP6ov=L?0cmYbbMtDkb-Zo%YvGINq1c7qs(;RlRl_xqSl*%$`JO~;Q1(B_&EJjok6 z4I|~dq*A)raEI7}1|Z)XPBPV1&?o2Cf6dzPHt)n9Yn!w#zBG}Xg8^gWOfs0qf>-4A z7~x}=shj~xhvs*we$vJj7`vtO!{*9>)qsBZXMd}oOgN~GQ=9GN0}6riuH;G!G8x4? z9CxEFXq+m^$uXnCB`9f5H_|0Is^JkOqC2Rs|f?oJTNhGFe3ag9PMx28(E6F=Y}&5ZqJ z8Z!;U9)p*DqpjzwTy7n@9dc$Rh@gH;Q|Byb8+}2b%H}^Q)O;=SR!}Rz&@b3Y@}o(7 z%HW|LMWj)n;BYOBzhZ9C+^S;`Y~!l;nn_0Xx8+VOKBLVm?f^IwU7)0qSx9|mGEuXL z^0_=|LqmT<^-{DEe{c{+``(+HeB^R1l)tNV-MKdLkgT}0p1+Q9cMRJt?UbamlZ>W) z(99!=Nd~L-9H*KK9t!=G&H&T2{G)@ad`vmW1>!#do}pE3&>*9$8&T+%_u9W}j30}Y zt1_&F;nhxiSV@N_MNp+I8sohmuwq|6!eQq1C@}agc~WVHJcqb6NGMgi zXs7qT8<#7yiM!byxBp`DR$j7nsooYpXOj7hL#h0dKk3@g^kvWq%$zkW3%6uIqFA?I z4*UdT<`n@_lUIu+=j)^*f@F*AB{KL4GF_)iY&9OS2k%NwFe}KtCe7P&gczT=?+#$r z^)}iW7)E+Qr+Pl(Er0>szK`->5oQLyHx6DZk0s=Q_d&WFsctXIIDQaC;r}B=*?NHku^kg~IfQ_+foM?_ zGvZmiq&VvTJ}wHhhup^~_>+4*N!Q#w?V@|;Qu9~F-sJ37JsNrFPW4K$`)Lz4vDqH~ zSVYF_T+NnrasX!nE-oU>!*t24*_-5AH?8|5T^~3Ce(7aw z*aVLMS*l56_X#H#7ggVVvEO>X@Rqna%P24Fi07)W-64iANx5A?J7wBZaU@RIx0l)A z{7!5k~rx1fsPnaDO|!6^DjP!UM~0u`ulGrxDV7h#U8!tVrd~K?*HXEKJ5U zEuBwu7*^P?&pVaW-fdY(b@WJAJ@uctlt&290Fq7Jpzb}gQ}DH*tYDBK=^$kujN2{x zY!b|FVuJApigX diff --git a/Stepic.xcodeproj/project.pbxproj b/Stepic.xcodeproj/project.pbxproj index e3e0befdc3..6eedae1ca9 100644 --- a/Stepic.xcodeproj/project.pbxproj +++ b/Stepic.xcodeproj/project.pbxproj @@ -15546,7 +15546,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 83; + CURRENT_PROJECT_VERSION = 84; DEVELOPMENT_TEAM = E9KER42773; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; @@ -15577,7 +15577,7 @@ CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 83; + CURRENT_PROJECT_VERSION = 84; DEVELOPMENT_TEAM = E9KER42773; ENABLE_BITCODE = YES; INFOPLIST_FILE = Stepic/Info.plist; diff --git a/Stepic/Info.plist b/Stepic/Info.plist index bc81589df4..c3033e1d9d 100644 --- a/Stepic/Info.plist +++ b/Stepic/Info.plist @@ -52,7 +52,7 @@ CFBundleVersion - 83 + 84 Fabric APIKey diff --git a/StepicAdaptiveCourse/Content/1838/AdaptiveInfo.plist b/StepicAdaptiveCourse/Content/1838/AdaptiveInfo.plist index fc8320847d..54a57c3d50 100644 --- a/StepicAdaptiveCourse/Content/1838/AdaptiveInfo.plist +++ b/StepicAdaptiveCourse/Content/1838/AdaptiveInfo.plist @@ -44,7 +44,7 @@ CFBundleVersion - 83 + 84 Fabric APIKey diff --git a/StepicAdaptiveCourse/Content/3124/AdaptiveInfo.plist b/StepicAdaptiveCourse/Content/3124/AdaptiveInfo.plist index 75b116f687..648e44e3ea 100644 --- a/StepicAdaptiveCourse/Content/3124/AdaptiveInfo.plist +++ b/StepicAdaptiveCourse/Content/3124/AdaptiveInfo.plist @@ -44,7 +44,7 @@ CFBundleVersion - 83 + 84 Fabric APIKey diff --git a/StepicAdaptiveCourse/Content/3149/AdaptiveInfo.plist b/StepicAdaptiveCourse/Content/3149/AdaptiveInfo.plist index 88a6a3442e..dd83efde59 100644 --- a/StepicAdaptiveCourse/Content/3149/AdaptiveInfo.plist +++ b/StepicAdaptiveCourse/Content/3149/AdaptiveInfo.plist @@ -44,7 +44,7 @@ CFBundleVersion - 83 + 84 Fabric APIKey diff --git a/StepicAdaptiveCourse/Content/3150/AdaptiveInfo.plist b/StepicAdaptiveCourse/Content/3150/AdaptiveInfo.plist index db1badd53a..90a395f578 100644 --- a/StepicAdaptiveCourse/Content/3150/AdaptiveInfo.plist +++ b/StepicAdaptiveCourse/Content/3150/AdaptiveInfo.plist @@ -44,7 +44,7 @@ CFBundleVersion - 83 + 84 Fabric APIKey diff --git a/StepicTests/Info.plist b/StepicTests/Info.plist index c7624a5fd5..06589914a8 100644 --- a/StepicTests/Info.plist +++ b/StepicTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 83 + 84 diff --git a/StepicWatch Extension/Info.plist b/StepicWatch Extension/Info.plist index 4b864e9895..00c0e04883 100644 --- a/StepicWatch Extension/Info.plist +++ b/StepicWatch Extension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.56 CFBundleVersion - 83 + 84 CLKComplicationPrincipalClass $(PRODUCT_MODULE_NAME).ComplicationController CLKComplicationSupportedFamilies diff --git a/StepicWatch/Info.plist b/StepicWatch/Info.plist index 7735c7f819..c9963822f3 100644 --- a/StepicWatch/Info.plist +++ b/StepicWatch/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.56 CFBundleVersion - 83 + 84 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait diff --git a/StepikTV/Info.plist b/StepikTV/Info.plist index 328f95b17f..83cdfff07c 100644 --- a/StepikTV/Info.plist +++ b/StepikTV/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString 1.56 CFBundleVersion - 83 + 84 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/StepikTVTests/Info.plist b/StepikTVTests/Info.plist index aead604be0..9da5ea716c 100644 --- a/StepikTVTests/Info.plist +++ b/StepikTVTests/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.56 CFBundleVersion - 83 + 84 diff --git a/StickerPackExtension/Info.plist b/StickerPackExtension/Info.plist index 7e9e5fa21c..f85ed99ca0 100644 --- a/StickerPackExtension/Info.plist +++ b/StickerPackExtension/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.56 CFBundleVersion - 83 + 84 NSExtension NSExtensionPointIdentifier diff --git a/UITests/Screenshots/Info.plist b/UITests/Screenshots/Info.plist index aba3dc31e1..6390de81a1 100644 --- a/UITests/Screenshots/Info.plist +++ b/UITests/Screenshots/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString 1.56 CFBundleVersion - 83 + 84 From 22bbebb2541109ad1692d35b8c27d391ce50865e Mon Sep 17 00:00:00 2001 From: Vladislav Kiryukhin Date: Thu, 5 Apr 2018 11:40:52 +0300 Subject: [PATCH 13/14] Fix loading placeholder for sections & units (#267) * Fix sections loading state * Fix units loading state * Add row deselect * Fix footerView restore --- Stepic/SectionsViewController.swift | 25 +++++++++++++++---------- Stepic/StepikTableView.swift | 9 ++++++++- Stepic/UnitsViewController.swift | 13 ++++++++++--- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/Stepic/SectionsViewController.swift b/Stepic/SectionsViewController.swift index a33f3c5f7e..3c73b95953 100644 --- a/Stepic/SectionsViewController.swift +++ b/Stepic/SectionsViewController.swift @@ -23,6 +23,8 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr private var shareTooltip: Tooltip? var didJustSubscribe: Bool = false + var isFirstLoad: Bool = true + private let notificationSuggestionManager = NotificationSuggestionManager() private let notificationPermissionManager = NotificationPermissionManager() @@ -55,7 +57,6 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr } refreshControl.layoutIfNeeded() refreshControl.beginRefreshing() - refreshSections() tableView.estimatedRowHeight = 44.0 tableView.rowHeight = UITableViewAutomaticDimension @@ -88,23 +89,28 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationItem.backBarButtonItem?.title = " " - tableView.reloadData() if(self.refreshControl.isRefreshing) { let offset = self.tableView.contentOffset self.refreshControl.endRefreshing() self.refreshControl.beginRefreshing() self.tableView.contentOffset = offset } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if isFirstLoad { + isFirstLoad = false + refreshSections() + } + if didRefresh { course.loadProgressesForSections(sections: course.sections, success: { [weak self] in self?.tableView.reloadData() - }, error: {}) + }, error: {}) } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) let shareTooltipBlock = { [weak self] in @@ -154,7 +160,7 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr shareTooltip?.dismiss() } - var emptyDatasetState: EmptyDatasetState = .empty { + var emptyDatasetState: EmptyDatasetState = .refreshing { didSet { switch emptyDatasetState { case .refreshing: @@ -175,7 +181,6 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr course.loadAllSections(success: { UIThread.performUI({ self.refreshControl.endRefreshing() - self.emptyDatasetState = EmptyDatasetState.empty self.tableView.reloadData() if let m = self.moduleId { if (1...self.course.sectionsArray.count ~= m) && (self.isReachable(section: m - 1)) { @@ -340,7 +345,7 @@ class SectionsViewController: UIViewController, ShareableController, UIViewContr extension SectionsViewController : UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { showSection(section: indexPath.row) - // tableView.deselectRow(at: indexPath, animated: true) + tableView.deselectRow(at: indexPath, animated: true) } func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { diff --git a/Stepic/StepikTableView.swift b/Stepic/StepikTableView.swift index 89f3e3623e..2f7b2019a5 100644 --- a/Stepic/StepikTableView.swift +++ b/Stepic/StepikTableView.swift @@ -24,6 +24,7 @@ class StepikTableView: UITableView { // Trick with removing cell separators: we should store previous footer to restore private var savedFooterView: UIView? + private var hasSavedFooter: Bool = false } extension StepikTableView { @@ -33,7 +34,11 @@ extension StepikTableView { private func handlePlaceholder(isHidden: Bool) { if isHidden { - tableFooterView = savedFooterView + if hasSavedFooter { + tableFooterView = savedFooterView + savedFooterView = nil + hasSavedFooter = false + } placeholderView.isHidden = true return } @@ -42,6 +47,8 @@ extension StepikTableView { // Remove cell separators savedFooterView = self.tableFooterView + hasSavedFooter = true + tableFooterView = UIView() placeholderView.isHidden = false } diff --git a/Stepic/UnitsViewController.swift b/Stepic/UnitsViewController.swift index 79c160d34a..023332e3ce 100644 --- a/Stepic/UnitsViewController.swift +++ b/Stepic/UnitsViewController.swift @@ -22,6 +22,7 @@ class UnitsViewController: UIViewController, ShareableController, UIViewControll var section: Section? var unitId: Int? + var isFirstLoad = true var didRefresh = false let refreshControl = UIRefreshControl() @@ -61,8 +62,6 @@ class UnitsViewController: UIViewController, ShareableController, UIViewControll refreshControl.beginRefreshing() - refreshUnits() - if(traitCollection.forceTouchCapability == .available) { registerForPreviewing(with: self, sourceView: view) } @@ -72,6 +71,15 @@ class UnitsViewController: UIViewController, ShareableController, UIViewControll } } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if isFirstLoad { + isFirstLoad = false + refreshUnits() + } + } + @objc func refresh() { refreshUnits() } @@ -242,7 +250,6 @@ class UnitsViewController: UIViewController, ShareableController, UIViewControll UIThread.performUI({ self.refreshControl.endRefreshing() self.tableView.reloadData() - self.emptyDatasetState = EmptyDatasetState.empty }) self.didRefresh = true success?() From 74feda27c9e83c060154254b7de878eae07116da Mon Sep 17 00:00:00 2001 From: Alexander Karpov Date: Fri, 6 Apr 2018 12:21:00 +0300 Subject: [PATCH 14/14] Added release notes (#268) --- fastlane/metadata/Stepic/en-US/release_notes.txt | 6 +++--- fastlane/metadata/Stepic/ru/release_notes.txt | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/fastlane/metadata/Stepic/en-US/release_notes.txt b/fastlane/metadata/Stepic/en-US/release_notes.txt index ad65b45259..50cf7a7b4e 100644 --- a/fastlane/metadata/Stepic/en-US/release_notes.txt +++ b/fastlane/metadata/Stepic/en-US/release_notes.txt @@ -1,8 +1,8 @@ In the new version of the Stepik iOS app we: -• Changed empty states to make every tiny thing nice and shiny -• Gave you a chance to understand that activity panel in profile is scrollable -• Improved onboarding for new users +• Changed all empty states. It's all perfectly beautiful now, check it out +• Added autocomplete for Kotlin, R and Haskell +• Sometimes app crashed after writing a comment. Now it doesn't • Fixed multiple bugs and greatly improved stability Enjoy learning! diff --git a/fastlane/metadata/Stepic/ru/release_notes.txt b/fastlane/metadata/Stepic/ru/release_notes.txt index 20092b6077..9273c09f6d 100644 --- a/fastlane/metadata/Stepic/ru/release_notes.txt +++ b/fastlane/metadata/Stepic/ru/release_notes.txt @@ -1,8 +1,8 @@ В новой версии Stepik для iOS мы: -• Переделали некоторые пустые состояния так, что все теперь кажется красивее (планируем переделать еще больше) -• Сделали возможность скролла панели активности в профиле более очевидной -• Улучшили онбординг для новых пользователей +• Переделали все пустые состояния так, что все теперь красивее +• Добавили автокомплит для Kotlin, R и Haskell +• Раньше иногда приложение вылетало, если оставить комментарий. Теперь не вылетает • Исправили множество ошибок и заметно улучшили стабильность Учитесь в удовольствие!