diff --git a/VocabularyTrainer.xcodeproj/project.pbxproj b/VocabularyTrainer.xcodeproj/project.pbxproj index ff3d564..89ced17 100644 --- a/VocabularyTrainer.xcodeproj/project.pbxproj +++ b/VocabularyTrainer.xcodeproj/project.pbxproj @@ -12,18 +12,20 @@ 28716F4E290EF54B00EACCA1 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28716F4B290EF54B00EACCA1 /* Coordinator.swift */; }; 28716F4F290EF54B00EACCA1 /* StoryBoarded.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28716F4C290EF54B00EACCA1 /* StoryBoarded.swift */; }; 28716F50290EF54B00EACCA1 /* MainCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28716F4D290EF54B00EACCA1 /* MainCoordinator.swift */; }; - 41B16E902A11617A00F83DB4 /* WaterfallTrueCompositionalLayout in Frameworks */ = {isa = PBXBuildFile; productRef = 41B16E8F2A11617A00F83DB4 /* WaterfallTrueCompositionalLayout */; }; B3606D1D294D3D55009E62E9 /* NewLanguageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3606D1C294D3D55009E62E9 /* NewLanguageViewController.swift */; }; B3657DFD290740D9000D76C8 /* AddNewWordViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3657DFC290740D8000D76C8 /* AddNewWordViewController.swift */; }; B38C29D4294D45F1006FCDEA /* Localizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38C29D3294D45F1006FCDEA /* Localizable.swift */; }; B38C29D8294D578C006FCDEA /* UILabel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38C29D7294D578C006FCDEA /* UILabel+Extensions.swift */; }; B38C29DA294D68B4006FCDEA /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38C29D9294D68B4006FCDEA /* String+Extensions.swift */; }; + B38FD6BA2A7721DB00DF1043 /* HomeEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38FD6B92A7721DB00DF1043 /* HomeEmptyView.swift */; }; + B38FD6C42A7740E700DF1043 /* TrainingEmptyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38FD6C32A7740E700DF1043 /* TrainingEmptyView.swift */; }; + B3E01CED298F260F004D86C1 /* TrainingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E01CEC298F260F004D86C1 /* TrainingView.swift */; }; + B3E01CEF298F2A7F004D86C1 /* TrainingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E01CEE298F2A7F004D86C1 /* TrainingViewController.swift */; }; B3E5421D294FB17A0015FD9C /* ModalCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E5421C294FB17A0015FD9C /* ModalCloseButton.swift */; }; B3E54220294FC3290015FD9C /* UIView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E5421F294FC3290015FD9C /* UIView+Extensions.swift */; }; B3E54224294FDBA60015FD9C /* UIAccessibility+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E54223294FDBA60015FD9C /* UIAccessibility+Extensions.swift */; }; CE0012FF2243F21C002F86DE /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0012FE2243F21C002F86DE /* UIViewController+Extensions.swift */; }; CE05F622229F17AF0097E3BB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CE05F624229F17AF0097E3BB /* Localizable.strings */; }; - CE0FE4F82238585A00430FDE /* TrainingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE0FE4F72238585A00430FDE /* TrainingViewController.swift */; }; CE2EB8272245406A006B1C6D /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = CE2EB8262245406A006B1C6D /* Launch Screen.storyboard */; }; CE31ED77290AC49F00AD3300 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE31ED76290AC49F00AD3300 /* AppDelegate.swift */; }; CE31ED79290AC4B100AD3300 /* AddWordDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE31ED78290AC4B100AD3300 /* AddWordDelegate.swift */; }; @@ -37,7 +39,7 @@ CEA8BCC52966108D00E2EAF9 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEA8BCC42966108D00E2EAF9 /* AboutViewController.swift */; }; CEB77943295E503F00D7BF35 /* NewLanguageScreenProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB77942295E503F00D7BF35 /* NewLanguageScreenProtocol.swift */; }; CEB77945295E563200D7BF35 /* UserDefaults+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB77944295E563200D7BF35 /* UserDefaults+Extensions.swift */; }; - CEB77947295E59E800D7BF35 /* EmojiChooser.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEB77946295E59E800D7BF35 /* EmojiChooser.swift */; }; + CEB77951295F681300D7BF35 /* WaterfallTrueCompositionalLayout in Frameworks */ = {isa = PBXBuildFile; productRef = CEB77950295F681300D7BF35 /* WaterfallTrueCompositionalLayout */; }; CECD6342235E54350060911F /* pp.html in Resources */ = {isa = PBXBuildFile; fileRef = CECD6344235E54350060911F /* pp.html */; }; CEE4988828F572E200B64FCF /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = CEE4988728F572E200B64FCF /* README.md */; }; CEF0605329510A27001D2CEA /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF0604E29510A26001D2CEA /* HomeViewController.swift */; }; @@ -78,15 +80,18 @@ B38C29D3294D45F1006FCDEA /* Localizable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localizable.swift; sourceTree = ""; }; B38C29D7294D578C006FCDEA /* UILabel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Extensions.swift"; sourceTree = ""; }; B38C29D9294D68B4006FCDEA /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; + B38FD6B92A7721DB00DF1043 /* HomeEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeEmptyView.swift; sourceTree = ""; }; + B38FD6C32A7740E700DF1043 /* TrainingEmptyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainingEmptyView.swift; sourceTree = ""; }; B3BBF4422909F64B004B2125 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = "pt-BR"; path = "pt-BR.lproj/pp.html"; sourceTree = ""; }; B3BBF4432909F653004B2125 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; + B3E01CEC298F260F004D86C1 /* TrainingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainingView.swift; sourceTree = ""; }; + B3E01CEE298F2A7F004D86C1 /* TrainingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainingViewController.swift; sourceTree = ""; }; B3E5421C294FB17A0015FD9C /* ModalCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalCloseButton.swift; sourceTree = ""; }; B3E5421F294FC3290015FD9C /* UIView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Extensions.swift"; sourceTree = ""; }; B3E54223294FDBA60015FD9C /* UIAccessibility+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAccessibility+Extensions.swift"; sourceTree = ""; }; CE0012FE2243F21C002F86DE /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Extensions.swift"; sourceTree = ""; }; CE05F623229F17AF0097E3BB /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CE05F625229F17C40097E3BB /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - CE0FE4F72238585A00430FDE /* TrainingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrainingViewController.swift; sourceTree = ""; }; CE2EB8262245406A006B1C6D /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; }; CE31ED76290AC49F00AD3300 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; CE31ED78290AC4B100AD3300 /* AddWordDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddWordDelegate.swift; sourceTree = ""; }; @@ -108,7 +113,6 @@ CEA8BCC42966108D00E2EAF9 /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; CEB77942295E503F00D7BF35 /* NewLanguageScreenProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewLanguageScreenProtocol.swift; sourceTree = ""; }; CEB77944295E563200D7BF35 /* UserDefaults+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Extensions.swift"; sourceTree = ""; }; - CEB77946295E59E800D7BF35 /* EmojiChooser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiChooser.swift; sourceTree = ""; }; CECD6343235E54350060911F /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = Base; path = Base.lproj/pp.html; sourceTree = ""; }; CECD6346235E54420060911F /* de */ = {isa = PBXFileReference; lastKnownFileType = text.html; name = de; path = de.lproj/pp.html; sourceTree = ""; }; CEE4988728F572E200B64FCF /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -126,7 +130,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 41B16E902A11617A00F83DB4 /* WaterfallTrueCompositionalLayout in Frameworks */, + CEB77951295F681300D7BF35 /* WaterfallTrueCompositionalLayout in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -161,7 +165,6 @@ isa = PBXGroup; children = ( B3606D1C294D3D55009E62E9 /* NewLanguageViewController.swift */, - CEB77946295E59E800D7BF35 /* EmojiChooser.swift */, ); path = NewLanguage; sourceTree = ""; @@ -198,6 +201,76 @@ path = Extensions; sourceTree = ""; }; + B38FD6BB2A77388000DF1043 /* View */ = { + isa = PBXGroup; + children = ( + CE431525295E08F4000F7FA2 /* CollectionViewCell.swift */, + CEF0605729510CD3001D2CEA /* HomeLanguageHeaderView.swift */, + B38FD6B92A7721DB00DF1043 /* HomeEmptyView.swift */, + ); + path = View; + sourceTree = ""; + }; + B38FD6BC2A77388E00DF1043 /* ViewModel */ = { + isa = PBXGroup; + children = ( + CEF0604F29510A26001D2CEA /* HomeViewModel.swift */, + CEF0605029510A26001D2CEA /* LanguageCellViewModel.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + B38FD6BD2A77389A00DF1043 /* ViewController */ = { + isa = PBXGroup; + children = ( + CEA8BCC42966108D00E2EAF9 /* AboutViewController.swift */, + CEF0604E29510A26001D2CEA /* HomeViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + B38FD6BE2A7738AF00DF1043 /* Model */ = { + isa = PBXGroup; + children = ( + CEF0605129510A27001D2CEA /* LanguageImport.swift */, + ); + path = Model; + sourceTree = ""; + }; + B38FD6BF2A7738BD00DF1043 /* Protocol */ = { + isa = PBXGroup; + children = ( + CEB77942295E503F00D7BF35 /* NewLanguageScreenProtocol.swift */, + ); + path = Protocol; + sourceTree = ""; + }; + B38FD6C02A773AED00DF1043 /* View */ = { + isa = PBXGroup; + children = ( + B3E01CEC298F260F004D86C1 /* TrainingView.swift */, + B38FD6C32A7740E700DF1043 /* TrainingEmptyView.swift */, + ); + path = View; + sourceTree = ""; + }; + B38FD6C12A773AF100DF1043 /* ViewController */ = { + isa = PBXGroup; + children = ( + B3E01CEE298F2A7F004D86C1 /* TrainingViewController.swift */, + ); + path = ViewController; + sourceTree = ""; + }; + B3E01CEB298F25FF004D86C1 /* Training */ = { + isa = PBXGroup; + children = ( + B38FD6C12A773AF100DF1043 /* ViewController */, + B38FD6C02A773AED00DF1043 /* View */, + ); + path = Training; + sourceTree = ""; + }; B3E5421E294FBD160015FD9C /* Components */ = { isa = PBXGroup; children = ( @@ -252,7 +325,7 @@ B38C29DB294D6A70006FCDEA /* Extensions */, B3606D1B294D3D3E009E62E9 /* NewLanguage */, B3657DFE290740F8000D76C8 /* AddWord */, - CE0FE4F72238585A00430FDE /* TrainingViewController.swift */, + B3E01CEB298F25FF004D86C1 /* Training */, CE2EB8262245406A006B1C6D /* Launch Screen.storyboard */, B3606D1E294D459E009E62E9 /* Localizable */, CECD6344235E54350060911F /* pp.html */, @@ -290,14 +363,11 @@ E37D1706261BCC22006C1F71 /* Home Screen */ = { isa = PBXGroup; children = ( - CE431525295E08F4000F7FA2 /* CollectionViewCell.swift */, - CEF0604E29510A26001D2CEA /* HomeViewController.swift */, - CEF0604F29510A26001D2CEA /* HomeViewModel.swift */, - CEF0605729510CD3001D2CEA /* HomeLanguageHeaderView.swift */, - CEF0605029510A26001D2CEA /* LanguageCellViewModel.swift */, - CEF0605129510A27001D2CEA /* LanguageImport.swift */, - CEB77942295E503F00D7BF35 /* NewLanguageScreenProtocol.swift */, - CEA8BCC42966108D00E2EAF9 /* AboutViewController.swift */, + B38FD6BF2A7738BD00DF1043 /* Protocol */, + B38FD6BE2A7738AF00DF1043 /* Model */, + B38FD6BD2A77389A00DF1043 /* ViewController */, + B38FD6BC2A77388E00DF1043 /* ViewModel */, + B38FD6BB2A77388000DF1043 /* View */, ); path = "Home Screen"; sourceTree = ""; @@ -320,7 +390,7 @@ ); name = VocabularyTrainer; packageProductDependencies = ( - 41B16E8F2A11617A00F83DB4 /* WaterfallTrueCompositionalLayout */, + CEB77950295F681300D7BF35 /* WaterfallTrueCompositionalLayout */, ); productName = VocabularyTrainer; productReference = CE86C5482235AE20008A5B73 /* VocabularyTrainer.app */; @@ -402,7 +472,7 @@ ); mainGroup = CE86C53F2235AE20008A5B73; packageReferences = ( - 41B16E8E2A11617A00F83DB4 /* XCRemoteSwiftPackageReference "WaterfallTrueCompositionalLayout" */, + CEB7794F295F681300D7BF35 /* XCRemoteSwiftPackageReference "WaterfallTrueCompositionalLayout" */, ); productRefGroup = CE86C5492235AE20008A5B73 /* Products */; projectDirPath = ""; @@ -472,6 +542,7 @@ buildActionMask = 2147483647; files = ( B3E54224294FDBA60015FD9C /* UIAccessibility+Extensions.swift in Sources */, + B38FD6BA2A7721DB00DF1043 /* HomeEmptyView.swift in Sources */, CEF0605629510A27001D2CEA /* LanguageImport.swift in Sources */, B3E54220294FC3290015FD9C /* UIView+Extensions.swift in Sources */, CE49D67E2963440300A8DDEB /* UIButton+Extensions.swift in Sources */, @@ -481,21 +552,22 @@ 28716F4E290EF54B00EACCA1 /* Coordinator.swift in Sources */, CEF0605829510CD3001D2CEA /* HomeLanguageHeaderView.swift in Sources */, CEF0605529510A27001D2CEA /* LanguageCellViewModel.swift in Sources */, + B3E01CED298F260F004D86C1 /* TrainingView.swift in Sources */, B38C29DA294D68B4006FCDEA /* String+Extensions.swift in Sources */, CE0012FF2243F21C002F86DE /* UIViewController+Extensions.swift in Sources */, B38C29D4294D45F1006FCDEA /* Localizable.swift in Sources */, + B38FD6C42A7740E700DF1043 /* TrainingEmptyView.swift in Sources */, CE31ED79290AC4B100AD3300 /* AddWordDelegate.swift in Sources */, CE431526295E08F4000F7FA2 /* CollectionViewCell.swift in Sources */, E37D170B261BCD59006C1F71 /* VocabularyDateFormatter.swift in Sources */, CEF0605329510A27001D2CEA /* HomeViewController.swift in Sources */, + B3E01CEF298F2A7F004D86C1 /* TrainingViewController.swift in Sources */, CEB77943295E503F00D7BF35 /* NewLanguageScreenProtocol.swift in Sources */, CEA8BCC52966108D00E2EAF9 /* AboutViewController.swift in Sources */, - CEB77947295E59E800D7BF35 /* EmojiChooser.swift in Sources */, B3E5421D294FB17A0015FD9C /* ModalCloseButton.swift in Sources */, B38C29D8294D578C006FCDEA /* UILabel+Extensions.swift in Sources */, CE60940A2999210F00C74933 /* Textfield.swift in Sources */, CEB77945295E563200D7BF35 /* UserDefaults+Extensions.swift in Sources */, - CE0FE4F82238585A00430FDE /* TrainingViewController.swift in Sources */, 18943A0A29094F470017E1A2 /* LanguageScreenViewController.swift in Sources */, 28716F4F290EF54B00EACCA1 /* StoryBoarded.swift in Sources */, CE86C5822235C971008A5B73 /* Helper.swift in Sources */, @@ -691,16 +763,15 @@ CODE_SIGN_ENTITLEMENTS = VocabularyTrainer/VocabularyTrainer.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; - DEVELOPMENT_TEAM = 6T68RPD4WZ; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = VocabularyTrainer/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.0; + MARKETING_VERSION = 1.3.1; PRODUCT_BUNDLE_IDENTIFIER = st.mic.VocabularyTrainer; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -716,16 +787,15 @@ CODE_SIGN_ENTITLEMENTS = VocabularyTrainer/VocabularyTrainer.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; - DEVELOPMENT_TEAM = 6T68RPD4WZ; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = VocabularyTrainer/Info.plist; - INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4.0; + MARKETING_VERSION = 1.3.1; PRODUCT_BUNDLE_IDENTIFIER = st.mic.VocabularyTrainer; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -781,7 +851,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 6T68RPD4WZ; + DEVELOPMENT_TEAM = 972LY7PTVT; INFOPLIST_FILE = VocabularyTrainerUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -801,7 +871,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 6T68RPD4WZ; + DEVELOPMENT_TEAM = 972LY7PTVT; INFOPLIST_FILE = VocabularyTrainerUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -858,20 +928,20 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 41B16E8E2A11617A00F83DB4 /* XCRemoteSwiftPackageReference "WaterfallTrueCompositionalLayout" */ = { + CEB7794F295F681300D7BF35 /* XCRemoteSwiftPackageReference "WaterfallTrueCompositionalLayout" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/eeshishko/WaterfallTrueCompositionalLayout.git"; + repositoryURL = "git@github.com:eeshishko/WaterfallTrueCompositionalLayout.git"; requirement = { - branch = main; - kind = branch; + kind = revision; + revision = f45da38f2f08aa557be75f64b1b5499149ae95d9; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 41B16E8F2A11617A00F83DB4 /* WaterfallTrueCompositionalLayout */ = { + CEB77950295F681300D7BF35 /* WaterfallTrueCompositionalLayout */ = { isa = XCSwiftPackageProductDependency; - package = 41B16E8E2A11617A00F83DB4 /* XCRemoteSwiftPackageReference "WaterfallTrueCompositionalLayout" */; + package = CEB7794F295F681300D7BF35 /* XCRemoteSwiftPackageReference "WaterfallTrueCompositionalLayout" */; productName = WaterfallTrueCompositionalLayout; }; /* End XCSwiftPackageProductDependency section */ diff --git a/VocabularyTrainer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/VocabularyTrainer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 487d2ee..fad8cb8 100644 --- a/VocabularyTrainer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/VocabularyTrainer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -3,9 +3,8 @@ { "identity" : "waterfalltruecompositionallayout", "kind" : "remoteSourceControl", - "location" : "https://github.com/eeshishko/WaterfallTrueCompositionalLayout.git", + "location" : "git@github.com:eeshishko/WaterfallTrueCompositionalLayout.git", "state" : { - "branch" : "main", "revision" : "f45da38f2f08aa557be75f64b1b5499149ae95d9" } } diff --git a/VocabularyTrainer/Assets.xcassets/Colors/closeButton.colorset/Contents.json b/VocabularyTrainer/Assets.xcassets/Colors/closeButton.colorset/Contents.json index 172b1e4..20d87df 100644 --- a/VocabularyTrainer/Assets.xcassets/Colors/closeButton.colorset/Contents.json +++ b/VocabularyTrainer/Assets.xcassets/Colors/closeButton.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.278", - "green" : "0.278", - "red" : "0.278" + "blue" : "0x47", + "green" : "0x47", + "red" : "0x47" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.741", - "green" : "0.741", - "red" : "0.741" + "blue" : "0xBD", + "green" : "0xBD", + "red" : "0xBD" } }, "idiom" : "universal" diff --git a/VocabularyTrainer/Assets.xcassets/Colors/grayButton.colorset/Contents.json b/VocabularyTrainer/Assets.xcassets/Colors/grayButton.colorset/Contents.json new file mode 100644 index 0000000..5c50be9 --- /dev/null +++ b/VocabularyTrainer/Assets.xcassets/Colors/grayButton.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.278", + "green" : "0.278", + "red" : "0.278" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xB9", + "green" : "0xB9", + "red" : "0xB9" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/VocabularyTrainer/Assets.xcassets/SadRobot.imageset/Contents.json b/VocabularyTrainer/Assets.xcassets/SadRobot.imageset/Contents.json new file mode 100644 index 0000000..a88f322 --- /dev/null +++ b/VocabularyTrainer/Assets.xcassets/SadRobot.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "SadRobot.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/VocabularyTrainer/Assets.xcassets/SadRobot.imageset/SadRobot.pdf b/VocabularyTrainer/Assets.xcassets/SadRobot.imageset/SadRobot.pdf new file mode 100644 index 0000000..25eccf0 Binary files /dev/null and b/VocabularyTrainer/Assets.xcassets/SadRobot.imageset/SadRobot.pdf differ diff --git a/VocabularyTrainer/Components/ModalCloseButton.swift b/VocabularyTrainer/Components/ModalCloseButton.swift index 6510749..08a33c5 100644 --- a/VocabularyTrainer/Components/ModalCloseButton.swift +++ b/VocabularyTrainer/Components/ModalCloseButton.swift @@ -24,5 +24,6 @@ final class ModalCloseButton: UIButton { layer.cornerRadius = 3 translatesAutoresizingMaskIntoConstraints = false accessibilityLabel = Localizable.close.localize() + accessibilityTraits = .button } } diff --git a/VocabularyTrainer/Coordinator/MainCoordinator.swift b/VocabularyTrainer/Coordinator/MainCoordinator.swift index 113b4b3..b87f29d 100644 --- a/VocabularyTrainer/Coordinator/MainCoordinator.swift +++ b/VocabularyTrainer/Coordinator/MainCoordinator.swift @@ -18,12 +18,10 @@ class MainCoordinator: Coordinator { self.navigationController = navigationController } - func start() { -// let homeVC = HomeScreenViewController() - let homeVC = HomeViewController(viewModel: HomeViewModel(coordinator: self)) -// homeVC.coordinator = self - navigationController.pushViewController(homeVC, animated: false) - } + func start() { + let homeVC = HomeViewController(viewModel: HomeViewModel(coordinator: self)) + navigationController.pushViewController(homeVC, animated: false) + } func navigateToNewLanguageViewController(newLanguageScreenProtocol: NewLanguageScreenProtocol) { let newLanguageVC = NewLanguageViewController(delegate: newLanguageScreenProtocol) @@ -55,11 +53,17 @@ class MainCoordinator: Coordinator { navigationController.pushViewController(addNewWordVC, animated: true) } - func navigateToTrainingViewController(with language: String) { - let trainingVC = TrainingViewController(with: language) - trainingVC.coordinator = self - navigationController.pushViewController(trainingVC, animated: true) - } + func navigateToTrainingViewController(with language: String) { + let trainingVC = TrainingViewController(with: language) + trainingVC.coordinator = self + trainingVC.modalPresentationStyle = .pageSheet + if #available(iOS 15.0, *) { + if let sheet = trainingVC.sheetPresentationController { + sheet.detents = [.medium(), .large()] + } + } + navigationController.present(trainingVC, animated: true) + } func popVC() { navigationController.popViewController(animated: true) diff --git a/VocabularyTrainer/Home Screen/LanguageImport.swift b/VocabularyTrainer/Home Screen/Model/LanguageImport.swift similarity index 100% rename from VocabularyTrainer/Home Screen/LanguageImport.swift rename to VocabularyTrainer/Home Screen/Model/LanguageImport.swift diff --git a/VocabularyTrainer/Home Screen/NewLanguageScreenProtocol.swift b/VocabularyTrainer/Home Screen/Protocol/NewLanguageScreenProtocol.swift similarity index 100% rename from VocabularyTrainer/Home Screen/NewLanguageScreenProtocol.swift rename to VocabularyTrainer/Home Screen/Protocol/NewLanguageScreenProtocol.swift diff --git a/VocabularyTrainer/Home Screen/CollectionViewCell.swift b/VocabularyTrainer/Home Screen/View/CollectionViewCell.swift similarity index 67% rename from VocabularyTrainer/Home Screen/CollectionViewCell.swift rename to VocabularyTrainer/Home Screen/View/CollectionViewCell.swift index 9428484..54471a0 100644 --- a/VocabularyTrainer/Home Screen/CollectionViewCell.swift +++ b/VocabularyTrainer/Home Screen/View/CollectionViewCell.swift @@ -33,24 +33,6 @@ class CollectionViewCell: UICollectionViewCell { return stackView }() - /// Image view showing the language's icon / image / emoji. - private let imageView: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - view.contentMode = .scaleAspectFit - view.image = UIImage(systemName: "hare") - view.clipsToBounds = true - return view - }() - - private let emojiLabel: UILabel = { - let label = UILabel() - label.numberOfLines = 1 - label.translatesAutoresizingMaskIntoConstraints = false - label.font = .preferredFont(forTextStyle: .headline) - return label - }() - private let background: UIView = { let view = UIView() view.backgroundColor = Colors.cellBackground @@ -77,7 +59,7 @@ class CollectionViewCell: UICollectionViewCell { func configure(with item: LanguageCellViewModel) { titleLabel.text = item.languageName subtitleLabel.text = item.subtitle - emojiLabel.text = item.emoji + contentView.accessibilityLabel = "\(item.languageName) \(item.subtitle)" } private func setUpUI() { @@ -88,18 +70,14 @@ class CollectionViewCell: UICollectionViewCell { labelContainer.addArrangedSubview(titleLabel) labelContainer.addArrangedSubview(subtitleLabel) contentView.addSubview(labelContainer) - contentView.addSubview(emojiLabel) + contentView.isAccessibilityElement = true + contentView.accessibilityTraits = [.button] } private func setUpConstraints() { NSLayoutConstraint.activate([ - emojiLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Dimensions.horizontalMargin), - emojiLabel.trailingAnchor.constraint(equalTo: labelContainer.leadingAnchor, constant: -Dimensions.horizontalMargin), - emojiLabel.centerYAnchor.constraint(equalTo: labelContainer.centerYAnchor), - emojiLabel.heightAnchor.constraint(equalToConstant: Dimensions.imageWidth), - emojiLabel.widthAnchor.constraint(equalToConstant: Dimensions.imageWidth), - labelContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Dimensions.verticalContainerMargin), + labelContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Dimensions.horizontalMargin * 2), labelContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Dimensions.horizontalMargin), labelContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Dimensions.verticalContainerMargin) ]) diff --git a/VocabularyTrainer/Home Screen/View/HomeEmptyView.swift b/VocabularyTrainer/Home Screen/View/HomeEmptyView.swift new file mode 100644 index 0000000..07deb4c --- /dev/null +++ b/VocabularyTrainer/Home Screen/View/HomeEmptyView.swift @@ -0,0 +1,74 @@ +// +// HomeEmptyView.swift +// VocabularyTrainer +// +// Created by Mariana Brasil on 30/07/23. +// Copyright © 2023 mic. All rights reserved. +// + +import UIKit + +final class HomeEmptyView: UIView { + + typealias Colors = HomeViewModel.Colors + + /// Image view showing the sad robot image. + private let imageView: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.contentMode = .scaleAspectFit + view.image = UIImage(named: "SadRobot") + view.clipsToBounds = true + view.isAccessibilityElement = false + return view + }() + + /// Empty label when the user don't have any language added. + private lazy var emptyLabel: UILabel = .createLabel(font: UIFontMetrics(forTextStyle: .body) + .scaledFont(for: .systemFont(ofSize: 14, weight: .regular)), + text: Localizable.emptyLanguage.localize(), + fontColor: Colors.subtitle, + textAlignment: .center) + + // MARK: - Initializer + + init() { + super.init(frame: .zero) + setupUI() + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + // MARK: - Setup + + private func setupUI() { + addSubviews([imageView, emptyLabel]) + translatesAutoresizingMaskIntoConstraints = false + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.topAnchor.constraint(equalTo: topAnchor), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageView.heightAnchor.constraint(equalToConstant: 70), + imageView.widthAnchor.constraint(equalToConstant: 80), + + emptyLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + emptyLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 28), + emptyLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + emptyLabel.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + public func startAnimation() { + UIView.animate(withDuration: 1, animations: { [weak self] in + self?.imageView.frame.origin.y -= 5 + }){ [weak self] _ in + UIView.animateKeyframes(withDuration: 1, delay: 0.1, options: [.autoreverse, .repeat], animations: { + self?.imageView.frame.origin.y += 5 + }) + } + } +} diff --git a/VocabularyTrainer/Home Screen/HomeLanguageHeaderView.swift b/VocabularyTrainer/Home Screen/View/HomeLanguageHeaderView.swift similarity index 83% rename from VocabularyTrainer/Home Screen/HomeLanguageHeaderView.swift rename to VocabularyTrainer/Home Screen/View/HomeLanguageHeaderView.swift index fe444a2..96d8e99 100644 --- a/VocabularyTrainer/Home Screen/HomeLanguageHeaderView.swift +++ b/VocabularyTrainer/Home Screen/View/HomeLanguageHeaderView.swift @@ -20,11 +20,18 @@ final class HomeLanguageHeaderView: UIView { typealias Colors = HomeViewModel.Colors /// Title with trailing add button. - private let titleLabel = UILabel() + private let titleLabel: UILabel = { + let label = UILabel() + label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) + label.text = Strings.headerTitle + label.accessibilityTraits = .header + return label + }() + /// Button for adding a new language. lazy var addLanguageButton: UIButton = { let button = UIButton(type: .contactAdd) - button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityLabel = Localizable.addNewLanguage.localize() button.tintColor = .label button.addAction( .init(handler: { [weak self] _ in @@ -37,29 +44,28 @@ final class HomeLanguageHeaderView: UIView { /// Button for starting practicing mode. lazy var practiceButton: UIButton = { let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false let text = NSAttributedString(string: Strings.practiceButtonTitle, attributes: [.underlineStyle: NSUnderlineStyle.single.rawValue] ) button.setAttributedTitle(text, for: .normal) button.tintColor = Colors.flippyGreen button.titleLabel?.font = .preferredFont(forTextStyle: .body) button.addAction(.init(handler: { [weak self] _ in self?.delegate?.tappedPracticeButton() }), for: .touchUpInside) + button.isHidden = true return button }() /// Button for editing a language. lazy var editButton: UIButton = { let button = UIButton(type: .system) - button.translatesAutoresizingMaskIntoConstraints = false button.setTitle(Strings.editButtonTitle, for: .normal) button.tintColor = .label button.addAction(.init(handler: { [weak self] _ in self?.delegate?.tappedEditButton() }), for: .touchUpInside) + button.isHidden = true return button }() /// Stack view horizontally aligning `practiceButton` and `editButton`. lazy var buttonStackView: UIStackView = { let stackView = UIStackView(arrangedSubviews: [practiceButton, editButton]) stackView.spacing = Layout.defaultMargin - stackView.translatesAutoresizingMaskIntoConstraints = false return stackView }() @@ -75,14 +81,16 @@ final class HomeLanguageHeaderView: UIView { required init?(coder: NSCoder) { nil } + func shouldHideHeaderButtons(_ isHidden: Bool) { + practiceButton.isHidden = isHidden + editButton.isHidden = isHidden + } + // MARK: - Setup private func setupUI() { - titleLabel.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: .systemFont(ofSize: 20, weight: .semibold)) - titleLabel.text = Strings.headerTitle - titleLabel.translatesAutoresizingMaskIntoConstraints = false - translatesAutoresizingMaskIntoConstraints = false addSubviews([titleLabel, addLanguageButton, buttonStackView]) + accessibilityElements = [titleLabel, addLanguageButton, buttonStackView] } private func setupConstraints() { @@ -91,15 +99,14 @@ final class HomeLanguageHeaderView: UIView { titleLabel.topAnchor.constraint(equalTo: topAnchor), titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor), - addLanguageButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8), + addLanguageButton.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor), addLanguageButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), addLanguageButton.heightAnchor.constraint(equalToConstant: 44), addLanguageButton.widthAnchor.constraint(equalToConstant: 44), buttonStackView.leadingAnchor.constraint(greaterThanOrEqualTo: addLanguageButton.trailingAnchor, constant: Layout.defaultMargin), - buttonStackView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor), - buttonStackView.trailingAnchor.constraint(equalTo: trailingAnchor), - buttonStackView.bottomAnchor.constraint(equalTo: bottomAnchor) + buttonStackView.topAnchor.constraint(greaterThanOrEqualTo: titleLabel.topAnchor, constant: -5), + buttonStackView.trailingAnchor.constraint(equalTo: trailingAnchor) ]) } } diff --git a/VocabularyTrainer/Home Screen/AboutViewController.swift b/VocabularyTrainer/Home Screen/ViewController/AboutViewController.swift similarity index 100% rename from VocabularyTrainer/Home Screen/AboutViewController.swift rename to VocabularyTrainer/Home Screen/ViewController/AboutViewController.swift diff --git a/VocabularyTrainer/Home Screen/HomeViewController.swift b/VocabularyTrainer/Home Screen/ViewController/HomeViewController.swift similarity index 68% rename from VocabularyTrainer/Home Screen/HomeViewController.swift rename to VocabularyTrainer/Home Screen/ViewController/HomeViewController.swift index a4b1f07..2a79d3e 100644 --- a/VocabularyTrainer/Home Screen/HomeViewController.swift +++ b/VocabularyTrainer/Home Screen/ViewController/HomeViewController.swift @@ -25,18 +25,45 @@ final class HomeViewController: UIViewController { private var selectedIndexPath: IndexPath? { collectionView.indexPathsForSelectedItems?.first } + /// If don't find any languages the isEmpty returns true + private var isLanguagesEmpty: Bool { + viewModel.data.isEmpty + } /// The currently selected language. private var selectedLanguage: String? { guard let selectedIndexPath = selectedIndexPath, viewModel.data.indices.contains(selectedIndexPath.row) else { return nil } return viewModel.data[selectedIndexPath.row].languageName } + /// The last index path selected. + private var selectedLastIndexPath: Int? + + private var accessibilityActions: [UIAccessibilityCustomAction]? { + let practiceAction = UIAccessibilityCustomAction(name: HomeViewModel.Strings.practiceButtonTitle, + target: self, + selector: #selector(practiceButtonAction)) + let editAction = UIAccessibilityCustomAction(name: HomeViewModel.Strings.editButtonTitle, + target: self, + selector: #selector(editButtonAction)) + let importAction = UIAccessibilityCustomAction(name: HomeViewModel.Strings.importButtonTitle) { _ in + self.selectFiles() + return true + } + let exportAction = UIAccessibilityCustomAction(name: HomeViewModel.Strings.exportButtonTitle, + target: self, + selector: #selector(tappedExport)) + return [practiceAction, editAction, importAction, exportAction] + } + + /// The empty view that will be displayed when there is no language added. + private lazy var emptyView: HomeEmptyView = .init() /// The collection view showing all the languages. private lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()) collectionView.autoresizingMask = [.flexibleHeight, .flexibleWidth] collectionView.backgroundColor = HomeViewModel.Colors.background collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self return collectionView }() /// Data source of `collectionView`. @@ -53,7 +80,7 @@ final class HomeViewController: UIViewController { return datasource }() /// Width of leading and trailing margins around `collectionView`. - private let horizontalCollectionViewMargins: CGFloat = 24 + private let horizontalCollectionViewMargins: CGFloat = 16 /// Colored Flippy logo shown at the leading side of the home screen's navbar. private let navbarLogo: UIBarButtonItem = { let label = UILabel() @@ -83,10 +110,6 @@ final class HomeViewController: UIViewController { self.headerView = HomeLanguageHeaderView() super.init(nibName: nil, bundle: nil) headerView.delegate = self - setNavigationItem() - setupView() - setConstraints() - applyCollectionViewChanges() } required init?(coder: NSCoder) { nil } @@ -96,14 +119,28 @@ final class HomeViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) removeNavigationBarBackground() + selectedLastIndexPath = nil + headerView.shouldHideHeaderButtons(true) + } + + override func viewDidLoad() { + super.viewDidLoad() + UIAccessibility.focusOn(navbarLogo) + setNavigationItem() + setupView() + setConstraints() + setEmptyState() + applyCollectionViewChanges() } // MARK: - Private Methods private func setupView() { view.backgroundColor = HomeViewModel.Colors.background - view.addSubview(headerView) - view.addSubview(collectionView) + view.addSubviews([headerView, collectionView]) + if isLanguagesEmpty { + view.addSubview(emptyView) + } view.addSubview(aboutButton) collectionView.setCollectionViewLayout(viewModel.layout, animated: true) } @@ -128,6 +165,20 @@ final class HomeViewController: UIViewController { ]) } + private func setEmptyState() { + guard isLanguagesEmpty else { return } + + if isLanguagesEmpty { + emptyView.startAnimation() + } + + NSLayoutConstraint.activate([ + emptyView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: horizontalCollectionViewMargins), + emptyView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 24), + emptyView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -horizontalCollectionViewMargins), + ]) + } + /// Applies changes of data source. func applyCollectionViewChanges() { var snap = datasource.snapshot() @@ -144,7 +195,6 @@ final class HomeViewController: UIViewController { navigationItem.leftBarButtonItem = navbarLogo let importButton = UIBarButtonItem( customView: UIButton.iconButton(type: .importButton) { [weak self] in -// self?.tappedImport() self?.selectFiles() } ) @@ -155,21 +205,22 @@ final class HomeViewController: UIViewController { ) navigationItem.rightBarButtonItems = [exportButton, importButton] } -} -// MARK: - HomeLanguageHeaderViewDelegate - -extension HomeViewController: HomeLanguageHeaderViewDelegate { - func tappedAddLanguageButton() { - viewModel.coordinator?.navigateToNewLanguageViewController(newLanguageScreenProtocol: self) + private func deselectCell(_ collectionView: UICollectionView, indexPath: IndexPath) { + collectionView.deselectItem(at: indexPath, animated: true) + collectionView.cellForItem(at: indexPath)?.contentView.accessibilityTraits.remove(.selected) + collectionView.cellForItem(at: indexPath)?.contentView.accessibilityCustomActions = nil + headerView.shouldHideHeaderButtons(true) + selectedLastIndexPath = nil } - func tappedPracticeButton() { + @objc private func practiceButtonAction() { guard let selectedLanguage = selectedLanguage else { return } viewModel.coordinator?.navigateToTrainingViewController(with: selectedLanguage) + selectedHapticFeedback() } - func tappedEditButton() { + @objc private func editButtonAction() { guard let selectedLanguage = selectedLanguage else { return } viewModel.coordinator?.navigateToLanguageScreenViewController( selectedLanguage: selectedLanguage, @@ -177,12 +228,51 @@ extension HomeViewController: HomeLanguageHeaderViewDelegate { completion: { [weak self] in self?.applyCollectionViewChanges() }) + selectedHapticFeedback() + } +} + +// MARK: - HomeLanguageHeaderViewDelegate + +extension HomeViewController: HomeLanguageHeaderViewDelegate { + func tappedAddLanguageButton() { + viewModel.coordinator?.navigateToNewLanguageViewController(newLanguageScreenProtocol: self) + selectedHapticFeedback() + } + + func tappedPracticeButton() { + practiceButtonAction() + } + + func tappedEditButton() { + editButtonAction() } } extension HomeViewController: NewLanguageScreenProtocol { func updateLanguageTable(language: String) { - self.applyCollectionViewChanges() + viewDidLoad() + applyCollectionViewChanges() + } +} + +extension HomeViewController: UICollectionViewDelegate { + + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + if selectedLastIndexPath == indexPath.row { + deselectCell(collectionView, indexPath: indexPath) + } else { + headerView.shouldHideHeaderButtons(false) + let cell = collectionView.cellForItem(at: indexPath)?.contentView + cell?.accessibilityTraits.insert(.selected) + cell?.accessibilityCustomActions = accessibilityActions + selectedLastIndexPath = indexPath.row + } + selectedHapticFeedback() + } + + func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + deselectCell(collectionView, indexPath: indexPath) } } @@ -191,28 +281,7 @@ extension HomeViewController: NewLanguageScreenProtocol { extension HomeViewController { -// private func tappedImport() { -// guard let files = ExportImport.getAllLanguageFileUrls() else { return } -// -// if files.isEmpty { -// let message = """ -// There were not found any language files for your app.\nFor a template of a language file you may create a new language with some vocabulary inside this app and export it. -// """ -// let alert = UIAlertController( -// title: NSLocalizedString("No language files found", -// comment: "No language files found"), -// message: NSLocalizedString(message, comment: message), -// preferredStyle: UIAlertController.Style.alert) -// -// alert.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.cancel, handler: nil)) -// self.present(alert, animated: true, completion: nil) -// } else { -// ExportImport.importLanguageFiles(files) -// applyCollectionViewChanges() -// } -// } - - private func tappedExport() { + @objc private func tappedExport() { if let selectedLanguage = selectedLanguage { let words = ExportImport.exportAsCsvToDocuments(language: selectedLanguage) @@ -221,6 +290,7 @@ extension HomeViewController { let ac = UIActivityViewController(activityItems: [cacheURL], applicationActivities: nil) present(ac, animated: true) + successHapticFeedback() } else { let alert = UIAlertController(title: NSLocalizedString("No language selected", comment: "Title for popup when no language was selected"), @@ -228,9 +298,25 @@ extension HomeViewController { preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) present(alert, animated: true, completion: nil) + errorHapticFeedback() } } - + + private func selectedHapticFeedback() { + let generator = UISelectionFeedbackGenerator() + generator.selectionChanged() + } + + private func successHapticFeedback() { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + } + + private func errorHapticFeedback() { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.error) + } + private func saveStringAsCSVToCacheDirectory(_ inputString: String, fileName: String) -> URL? { // Get the cache directory URL if let cacheDirectoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first { diff --git a/VocabularyTrainer/Home Screen/HomeViewModel.swift b/VocabularyTrainer/Home Screen/ViewModel/HomeViewModel.swift similarity index 94% rename from VocabularyTrainer/Home Screen/HomeViewModel.swift rename to VocabularyTrainer/Home Screen/ViewModel/HomeViewModel.swift index ef859fc..6022552 100644 --- a/VocabularyTrainer/Home Screen/HomeViewModel.swift +++ b/VocabularyTrainer/Home Screen/ViewModel/HomeViewModel.swift @@ -20,10 +20,8 @@ final class HomeViewModel { if let languages = UserDefaults.standard.array(forKey: UserDefaultKeys.languages) as? [String] { retVal = languages.map { let languageDict = UserDefaults.standard.dictionary(forKey: $0) as? [String: String] - let emoji = UserDefaults.standard.languageEmoji(for: $0) ?? "" return LanguageCellViewModel(languageName: $0, - numberOfWords: languageDict?.count ?? 0, - emoji: emoji) + numberOfWords: languageDict?.count ?? 0) } } return retVal @@ -74,5 +72,6 @@ final class HomeViewModel { static let title = UIColor.label static let subtitle = UIColor.secondaryLabel static let flippyGreen = UIColor(named: "flippyGreen") + static let selectedCell = UIColor(named: "selectedCell") } } diff --git a/VocabularyTrainer/Home Screen/LanguageCellViewModel.swift b/VocabularyTrainer/Home Screen/ViewModel/LanguageCellViewModel.swift similarity index 75% rename from VocabularyTrainer/Home Screen/LanguageCellViewModel.swift rename to VocabularyTrainer/Home Screen/ViewModel/LanguageCellViewModel.swift index dbb9b53..ccb2e45 100644 --- a/VocabularyTrainer/Home Screen/LanguageCellViewModel.swift +++ b/VocabularyTrainer/Home Screen/ViewModel/LanguageCellViewModel.swift @@ -12,13 +12,17 @@ struct LanguageCellViewModel: Hashable { let id = UUID() let languageName: String let numberOfWords: Int - let emoji: String /// Subtitle containing word counter, // TODO: use plural localization var subtitle: String { - String(format: LanguageCellViewModel.Strings.numberOfWordsTitle, - numberOfWords) + if numberOfWords == 1 { + return String(format: LanguageCellViewModel.Strings.numberOfWordTitle, + numberOfWords) + } else { + return String(format: LanguageCellViewModel.Strings.numberOfWordsTitle, + numberOfWords) + } } /// Calculates required height of cell for given `width` based on text to be rendered in the cell. @@ -48,7 +52,13 @@ extension LanguageCellViewModel { enum Strings { static let numberOfWordsTitle = NSLocalizedString( "homescreen_language_subtitle", - value: "Words: %i", + value: NSLocalizedString("%d words", comment: "%d words"), + comment: "Subtitle of a cell on the home screen showing the word count of a language." + ) + + static let numberOfWordTitle = NSLocalizedString( + "homescreen_language_subtitle", + value: NSLocalizedString("%d word", comment: "%d word"), comment: "Subtitle of a cell on the home screen showing the word count of a language." ) } diff --git a/VocabularyTrainer/Localizable/Localizable.swift b/VocabularyTrainer/Localizable/Localizable.swift index 3dc1824..f06646f 100644 --- a/VocabularyTrainer/Localizable/Localizable.swift +++ b/VocabularyTrainer/Localizable/Localizable.swift @@ -16,6 +16,17 @@ enum Localizable: String { case language case languageExists case add + case translation + case answer + case check + case nextWord + case emptyLanguage + case emptyWord + case skip + case takeLook + case addedLanguage + case wrongAnswer + case rightAnswer func localize() -> String { return NSLocalizedString(self.rawValue, comment: "") diff --git a/VocabularyTrainer/Localizable/de.lproj/Localizable.strings b/VocabularyTrainer/Localizable/de.lproj/Localizable.strings index fcd12e1..ac04bc5 100644 --- a/VocabularyTrainer/Localizable/de.lproj/Localizable.strings +++ b/VocabularyTrainer/Localizable/de.lproj/Localizable.strings @@ -46,12 +46,11 @@ "⤵ import" = "⤋ importieren"; "↑ export" = "⇪ exportieren"; "answer" = "Antwort"; -"Check" = "Prüfen"; -"Take a look" = "Umdrehen"; -"Skip word" = "Wort überspringen"; +"check" = "Prüfen"; +"takeLook" = "Umdrehen"; +"skip" = "Wort überspringen"; "search for words" = "Wort suchen"; "Importing language files into the app will overwrite any languages in your app with the same name as the csv-file.\n Do you want to proceed?" = "Auf dem iPhone vorhandene Sprachen mit gleichem Namen wie die zu importierenden csv-Dateien werden beim Importieren überschrieben.\nWillst du die csv-Dateien wirklich importieren?"; -"There were not found any language files for your app.\nFor a template of a language file you may create a new language with some vocabulary inside this app and export it." = "Es konnten keine Sprachdateien für die App gefunden werden. Um eine Vorlage für eine Sprachdatei zu erhalten, erstelle eine neue Sprache in der App und exportiere diese."; "No language files found" = "Keine Sprachdateien gefunden"; "👍 I was right" = "👍"; "👎 I was wrong" = "👎"; @@ -70,5 +69,10 @@ "home_edit_button_title" = "Bearbeiten"; "home_import_button_title" = "Importieren"; "home_export_button_title" = "Exportieren"; -"alert_import_language_existing_message" = "Es gibt bereits eine Vokabelset mit Namen %@. Dieses wird beim Importieren überschrieben. Willst du fortfahren?"; -"alert_import_language_existing_title" = "Existiert bereits"; +"emptyMessage" = "Es konnten keine Sprachdateien für die App gefunden werden. Um eine Vorlage für eine Sprachdatei zu erhalten, erstelle eine neue Sprache in der App und exportiere diese."; +"nextWord" = "Nächstes Wort"; +"emptyLanguage" = "Du hast noch keine Sprachen zum Üben ausgewählt! \n Tippe auf die + Schaltfläche, um deine Lernreise zu beginnen."; +"emptyWord" = "Du hast noch keine Wörter zum Üben ausgewählt! \n Tippe auf die Bearbeiten-Schaltfläche, um neue Wörter hinzuzufügen."; +"addedLanguage" = "Sprache hinzugefügt"; +"alert_import_language_existing_message" = "Du hast bereits eine Sprache mit dem Namen %@. Diese wird beim Import überschrieben.\Bist du sicher, dass Du fortfahren möchtest?"; +"alert_import_language_existing_title" = "Bereits vorhanden"; diff --git a/VocabularyTrainer/Localizable/en.lproj/Localizable.strings b/VocabularyTrainer/Localizable/en.lproj/Localizable.strings index 06a6dcf..eac243e 100644 --- a/VocabularyTrainer/Localizable/en.lproj/Localizable.strings +++ b/VocabularyTrainer/Localizable/en.lproj/Localizable.strings @@ -23,9 +23,9 @@ "0 words" = "0 words"; "My words" = "⚙️ Edit"; "new Word" = "new word"; -"translation" = "translation"; +"translation" = "Translation"; "Training" = "📖 Training"; -"Training:" = "Training:"; +"Training" = "Training"; "Delete Language" = "🗑 Delete Language"; "export" = "💾 export language"; "New word" = "New word"; @@ -45,14 +45,14 @@ "You may copy your file to your machine via iTunes:\n iPhone->Filesharing->Flippy->drag csv-files into Finder" = "You may copy your file to your machine via iTunes:\n iPhone->Filesharing->Flippy->drag csv-files into Finder"; "⤵ import" = "⤋ import"; "↑ export" = "⇪ export"; -"answer" = "answer"; -"Check" = "Check"; -"Take a look" = "Take a look"; -"Skip word" = "Skip word"; +"answer" = "Answer"; +"check" = "Check"; +"takeLook" = "Take a look"; +"skip" = "Skip word"; "search for words" = "search for words"; "Importing language files into the app will overwrite any languages in your app with the same name as the csv-file.\n Do you want to proceed?" = "Existing languages on your device with the same name as the csv-file you try to import will be overwritten.\n Do you want to proceed?"; -"There were not found any language files for your app.\nFor a template of a language file you may create a new language with some vocabulary inside this app and export it." = "There were not found any language files for your app.\nFor a template of a language file you may create a new language with some vocabulary inside this app and export it."; +"emptyMessage" = "There were not found any language files for your app.\nFor a template of a language file you may create a new language with some vocabulary inside this app and export it."; "No language files found" = "No language files found"; "👍 I was right" = "👍"; "👎 I was wrong" = "👎"; @@ -66,12 +66,16 @@ "Please wait..." = "Please wait..."; "Edit word" = "✏️ Edit word"; "Your progress and probability configuration for this word will be saved" = "Your progress and probability configuration for this word will be saved"; -"That's wrong 😕" = "That's wrong 😕"; -"That's right! 🤠" = "That's right! 🤠"; +"wrongAnswer" = "That's wrong"; +"rightAnswer" = "That's right!"; "home_header_title" = "My Languages"; "home_practice_button_title" = "Practice"; "home_edit_button_title" = "Edit"; "home_import_button_title" = "Import"; "home_export_button_title" = "Export"; +"nextWord" = "Next word"; +"emptyLanguage" = "You don't have any languages to practice, yet! \n Tap the + button to start your learning journey."; +"emptyWord" = "You don't have any words to practice, yet! \n Tap the edit button to add new words."; +"addedLanguage" = "Added Language"; "alert_import_language_existing_message" = "You already have a language called %@. This will be overwritten when importing.\nAre you sure you want to proceed?"; "alert_import_language_existing_title" = "Already existing"; diff --git a/VocabularyTrainer/Localizable/hi.lproj/Localizable.strings b/VocabularyTrainer/Localizable/hi.lproj/Localizable.strings index 157d4cb..82588b0 100644 --- a/VocabularyTrainer/Localizable/hi.lproj/Localizable.strings +++ b/VocabularyTrainer/Localizable/hi.lproj/Localizable.strings @@ -22,7 +22,7 @@ "new Word" = "नया शब्द"; "translation" = "अनुवाद"; "Training" = "📖 प्रशिक्षण"; -"Training:" = "प्रशिक्षण:"; +"Training" = "प्रशिक्षण"; "Delete Language" = "🗑 भाषा हटाएं"; "export" = "💾 export language"; "New word" = "नया शब्द"; @@ -43,13 +43,13 @@ "⤵ import" = "⤋ import"; "↑ export" = "⇪ export"; "answer" = "उत्तर"; -"Check" = "जांच"; -"Take a look" = "एक नज़र डालें"; -"Skip word" = "शब्द छोड़ें"; +"check" = "जांच"; +"takeLook" = "एक नज़र डालें"; +"skip" = "शब्द छोड़ें"; "search for words" = "शब्द खोजें"; "Importing language files into the app will overwrite any languages in your app with the same name as the csv-file.\n Do you want to proceed?" = "आपके device पर उसी नाम वाली मौजूदा भाषाएं, जिसका नाम आप जिस csv file import करने का प्रयास करते हैं, उसे अधिलेखित कर दिया जाएगा।\n क्या आप आगे बढ़ना चाहते हैं?"; -"There were not found any language files for your app.\nFor a template of a language file you may create a new language with some vocabulary inside this app and export it." = "आपके ऐप के लिए कोई भाषा फ़ाइल नहीं मिली। \n किसी भाषा फ़ाइल के template के लिए आप इस app के अंदर कुछ शब्दावली एक नई भाषा बना सकते हैं और इसे export कर सकते हैं।"; +"emptyMessage" = "आपके ऐप के लिए कोई भाषा फ़ाइल नहीं मिली। \n किसी भाषा फ़ाइल के template के लिए आप इस app के अंदर कुछ शब्दावली एक नई भाषा बना सकते हैं और इसे export कर सकते हैं।"; "No language files found" = "कोई भाषा फ़ाइल नहीं मिली"; "👍 I was right" = "👍"; "👎 I was wrong" = "👎"; @@ -63,12 +63,16 @@ "Please wait..." = "कृपया प्रतीक्षा करें..."; "Edit word" = "✏️ वर्ड एडिट करें"; "Your progress and probability configuration for this word will be saved" = "इस शब्द के लिए आपकी प्रगति और profile configuration सहेजा जाएगा"; -"That's wrong 😕" = "यह गलत है 😕"; -"That's right! 🤠" = "यह सही है! 🤠"; +"wrongAnswer" = "यह गलत है"; +"rightAnswer" = "यह सही है!"; "home_header_title" = "My Languages"; -"home_practice_button_title" = "Practice"; +"home_practice_button_title" = "प्रशिक्षण"; "home_edit_button_title" = "Edit"; "home_import_button_title" = "Import"; "home_export_button_title" = "Export"; -"alert_import_language_existing_message" = "You already have a language called %@. This will be overwritten when importing.\nAre you sure you want to proceed?"; -"alert_import_language_existing_title" = "Already existing"; +"nextWord" = "अगला शब्द"; +"emptyLanguage" = "आपके पास अभी तक कोई भाषाएँ अभ्यास करने के लिए उपलब्ध नहीं हैं! \n + बटन दबाकर अपनी सीखने की यात्रा शुरू करें।"; +"emptyWord" = "आपके पास अभी तक कोई शब्द अभ्यास करने के लिए नहीं हैं! \n नए शब्द जोड़ने के लिए संपादित बटन दबाएं।"; +"addedLanguage" = "भाषा जोड़ी गई"; +"alert_import_language_existing_message" = "आपके पास पहले से ही एक भाषा है जिसका नाम %@ है। इसे आयात करते समय यह ओवरराइट हो जाएगा।\nक्या आप निश्चित ही आगे बढ़ना चाहते हो?"; +"alert_import_language_existing_title" = "पहले से मौजूद है"; diff --git a/VocabularyTrainer/Localizable/nl.lproj/Localizable.strings b/VocabularyTrainer/Localizable/nl.lproj/Localizable.strings index d78b0ac..ef21897 100644 --- a/VocabularyTrainer/Localizable/nl.lproj/Localizable.strings +++ b/VocabularyTrainer/Localizable/nl.lproj/Localizable.strings @@ -22,7 +22,7 @@ "0 words" = "0 woorden"; "My words" = "⚙️ Bewerken"; "new Word" = "nieuw woord"; -"translation" = "vertaling"; +"translation" = "Vertaling"; "Training" = "📖 Training"; "Delete Language" = "🗑 Taal verwijderen"; "export" = "💾 Taal exporteren"; @@ -43,14 +43,14 @@ "You may copy your file to your machine via iTunes:\n iPhone->Filesharing->Flippy->drag csv-files into Finder" = "Je kan je taal bestand kopeeren naar je apparaat via iTunes:\n iPhone->Filesharing->Flippy->drag csv-files naar Finder"; "⤵ import" = "⤋ importeren"; "↑ export" = "⇪ exporteren"; -"answer" = "antwoord"; -"Check" = "Controleer"; -"Take a look" = "Neem een kijkje"; -"Skip word" = "Woord overslaan"; +"answer" = "Antwoord"; +"check" = "Controleer"; +"takeLook" = "Neem een kijkje"; +"skip" = "Woord overslaan"; "search for words" = "Zoeken naar woorden"; "Importing language files into the app will overwrite any languages in your app with the same name as the csv-file.\n Do you want to proceed?" = "Bestaande talen op je apparaat met dezelfde naam als het csv-file worden overschreven.\n Is dit oke?"; -"There were not found any language files for your app.\nFor a template of a language file you may create a new language with some vocabulary inside this app and export it." = "Geen bestanden met talen gevonden voor deze app.\nOm een template te creeren, kan je een nieuwe taal aanmaken en exporteren."; +"emptyMessage" = "Geen bestanden met talen gevonden voor deze app.\nOm een template te creeren, kan je een nieuwe taal aanmaken en exporteren."; "No language files found" = "Geen taalbestand geselecteerd"; "👍 I was right" = "👍"; "👎 I was wrong" = "👎"; @@ -69,5 +69,9 @@ "home_edit_button_title" = "Edit"; "home_import_button_title" = "Import"; "home_export_button_title" = "Export"; -"alert_import_language_existing_message" = "You already have a language called %@. This will be overwritten when importing.\nAre you sure you want to proceed?"; -"alert_import_language_existing_title" = "Already existing"; +"nextWord" = "Volgende woord"; +"emptyLanguage" = "Je hebt nog geen talen om te oefenen! \n Tik op de + knop om aan je leerreis te beginnen."; +"emptyWord" = "Je hebt nog geen woorden om te oefenen! \n Tik op de bewerk-knop om nieuwe woorden toe te voegen."; +"addedLanguage" = "Taal toegevoegd"; +"alert_import_language_existing_message" = "Je hebt al een taal met de naam %@. Dit wordt overschreven bij importeren.\nWeet je zeker dat je wilt doorgaan?"; +"alert_import_language_existing_title" = "Al reeds aanwezig"; diff --git a/VocabularyTrainer/Localizable/pt-BR.lproj/Localizable.strings b/VocabularyTrainer/Localizable/pt-BR.lproj/Localizable.strings index e78b632..88b0181 100644 --- a/VocabularyTrainer/Localizable/pt-BR.lproj/Localizable.strings +++ b/VocabularyTrainer/Localizable/pt-BR.lproj/Localizable.strings @@ -23,9 +23,9 @@ "0 words" = "0 palavras"; "My words" = "⚙️ Editar"; "new Word" = "nova palavra"; -"translation" = "tradução"; +"translation" = "Tradução"; "Training" = "📖 Treinar"; -"Training:" = "Treinando:"; +"Training" = "Treinando"; "Delete Language" = "🗑 Excluir idioma"; "export" = "💾 exportar idioma"; "New word" = "Nova palavra"; @@ -45,13 +45,13 @@ "You may copy your file to your machine via iTunes:\n iPhone->Filesharing->Flippy->drag csv-files into Finder" = "Você pode copiar o arquivo para o seu computador pelo iTunes.\nAcesse: iPhone->Compartilhamento de Arquivos->Flippy->arraste csv-files para o Finder"; "⤵ import" = "⤋ importar"; "↑ export" = "⇪ exportar"; -"answer" = "resposta"; -"Check" = "Confirmar"; -"Take a look" = "Espiar"; -"Skip word" = "Pular palavra"; +"answer" = "Resposta"; +"check" = "Confirmar"; +"takeLook" = "Espiar"; +"skip" = "Pular palavra"; "search for words" = "buscar palavras"; "Importing language files into the app will overwrite any languages in your app with the same name as the csv-file.\n Do you want to proceed?" = "Os idiomas existentes no seu aparelho caso possuam o mesmo nome que os arquivos csv-file a serem importados serão sobrescritos.\n Você gostaria de continuar?"; -"There were not found any language files for your app.\nFor a template of a language file you may create a new language with some vocabulary inside this app and export it." = "Não foi encontrado qualquer arquivo de idioma para o seu app.\nPara ter o modelo do arquivo de idioma, você terá que criar um novo idioma com algumas palavras adicionadas dentro do app e exportar."; +"emptyMessage" = "Não foi encontrado qualquer arquivo de idioma para o seu app.\nPara ter o modelo do arquivo de idioma, você terá que criar um novo idioma com algumas palavras adicionadas dentro do app e exportar."; "No language files found" = "Nenhum arquivo de idioma foi encontrado"; "👍 I was right" = "👍"; "👎 I was wrong" = "👎"; @@ -65,12 +65,17 @@ "Please wait..." = "Por favor, aguarde..."; "Edit word" = "✏️ Editar palavra"; "Your progress and probability configuration for this word will be saved" = "Seu progresso e configuração de probabilidade para essa palavra não será alterado com essa mudança"; -"That's wrong 😕" = "Errado 😕"; -"That's right! 🤠" = "Acertou! 🤠"; -"home_header_title" = "My Languages"; -"home_practice_button_title" = "Practice"; -"home_edit_button_title" = "Edit"; +"wrongAnswer" = "Errado"; +"rightAnswer" = "Acertou!"; +"home_header_title" = "Meus Idiomas"; +"home_practice_button_title" = "Praticar"; +"home_edit_button_title" = "Editar"; "home_import_button_title" = "Importar"; +"home_export_button_title" = "Exportar"; +"nextWord" = "Próxima palavra"; +"emptyLanguage" = "Você não tem nenhum idioma para praticar, ainda! \n Toque no botão + para começar sua jornada de aprendizado."; +"emptyWord" = "Você não tem nenhuma palavra para praticar, ainda! \n Toque no botão editar para adicionar novas palavras."; +"addedLanguage" = "Idioma adicionado"; "home_export_button_title" = "Export"; -"alert_import_language_existing_message" = "You already have a language called %@. This will be overwritten when importing.\nAre you sure you want to proceed?"; -"alert_import_language_existing_title" = "Already existing"; +"alert_import_language_existing_message" = "Você já tem um idioma chamado %@. Isso será sobrescrito ao importar.\nVocê tem certeza que quer prosseguir?"; +"alert_import_language_existing_title" = "Já existente"; diff --git a/VocabularyTrainer/NewLanguage/EmojiChooser.swift b/VocabularyTrainer/NewLanguage/EmojiChooser.swift deleted file mode 100644 index e8f5abc..0000000 --- a/VocabularyTrainer/NewLanguage/EmojiChooser.swift +++ /dev/null @@ -1,303 +0,0 @@ -// -// EmojiChooser.swift -// VocabularyTrainer -// -// Created by skrr on 30.12.22. -// Copyright © 2022 mic. All rights reserved. -// - -import Foundation - -enum EmojiChooser { - - /// Huge set of emoji that look friendly. - private static let emojiSet: Set = [ - "😀", - "😃", - "😄", - "😁", - "😆", - "😅", - "🤣", - "😂", - "🙂", - "🙃", - "😉", - "😊", - "😇", - "🥰", - "😍", - "🤩", - "😘", - "😗", - "☺️", - "😚", - "😙", - "🥲", - "😋", - "😛", - "😜", - "🤪", - "😝", - "🤑", - "🤗", - "🍇", - "🍈", - "🍉", - "🍊", - "🍋", - "🍌", - "🍍", - "🥭", - "🍎", - "🍏", - "🍐", - "🍑", - "🍒", - "🍓", - "🫐", - "🥝", - "🍅", - "🫒", - "🥥", - "🥑", - "🍆", - "🥔", - "🥕", - "🌽", - "🌶️", - "🫑", - "🥒", - "🥬", - "🥦", - "🧄", - "🧅", - "🍄", - "🥜", - "🌰", - "🍞", - "🥐", - "🥖", - "🫓", - "🥨", - "🥯", - "🥞", - "🧇", - "🧀", - "🍖", - "🍗", - "🥩", - "🥓", - "🍔", - "🍟", - "🍕", - "🌭", - "🥪", - "🌮", - "🌯", - "🫔", - "🥙", - "🧆", - "🥚", - "🍳", - "🥘", - "🍲", - "🫕", - "🥣", - "🥗", - "🍿", - "🧈", - "🧂", - "🥫", - "🍱", - "🍘", - "🍙", - "🍚", - "🍛", - "🍜", - "🍝", - "🍠", - "🍢", - "🍣", - "🍤", - "🍥", - "🥮", - "🍡", - "🥟", - "🥠", - "🥡", - "🦪", - "🍦", - "🍧", - "🍨", - "🍩", - "🍪", - "🎂", - "🍰", - "🧁", - "🥧", - "🍫", - "🍬", - "🍭", - "🍮", - "🍯", - "🍼", - "🥛", - "☕", - "🫖", - "🍵", - "🍶", - "🍾", - "🍷", - "🍸", - "🍹", - "🍺", - "🍻", - "🥂", - "🥃", - "🫗", - "🥤", - "🧋", - "🧃", - "🧉", - "🧊", - "🥢", - "🍽️", - "🍴", - "🥄", - "🚣", - "🗾", - "🏔️", - "⛰️", - "🌋", - "🗻", - "🏕️", - "🏖️", - "🏜️", - "🏝️", - "🏞️", - "🏟️", - "🏛️", - "🏗️", - "🛖", - "🏘️", - "🏚️", - "🏠", - "🏡", - "🏢", - "🏣", - "🏤", - "🏥", - "🏦", - "🏨", - "🏩", - "🏪", - "🏫", - "🏬", - "🏭", - "🏯", - "🏰", - "💒", - "🗼", - "🗽", - "⛪", - "🕌", - "🛕", - "🕍", - "⛩️", - "🕋", - "⛲", - "⛺", - "🌁", - "🌃", - "🏙️", - "🌄", - "🌅", - "🌆", - "🌇", - "🌉", - "🎠", - "🎡", - "🎢", - "🚂", - "🚃", - "🚄", - "🚅", - "🚆", - "🚇", - "🚈", - "🚉", - "🚊", - "🚝", - "🚞", - "🚋", - "🚌", - "🚍", - "🚎", - "🚐", - "🚑", - "🚒", - "🚓", - "🚔", - "🚕", - "🚖", - "🚗", - "🚘", - "🚙", - "🛻", - "🚚", - "🚛", - "🚜", - "🏎️", - "🏍️", - "🛵", - "🛺", - "🚲", - "🛴", - "🚏", - "🛣️", - "🛤️", - "⛽", - "🚨", - "🚥", - "🚦", - "🚧", - "⚓", - "⛵", - "🚤", - "🛳️", - "⛴️", - "🛥️", - "🚢", - "✈️", - "🛩️", - "🛫", - "🛬", - "🪂", - "💺", - "🚁", - "🚟", - "🚠", - "🚡", - "🛰️", - "🚀", - "🛸", - "🪐", - "🌠", - "🌌", - "⛱️", - "🎆", - "🎇", - "🎑", - "💴", - "💵", - "💶", - "💷", - "🗿" - ] - - /// Randomly chooses an emoji from `emojiSet`. - static var choose: String { - emojiSet.randomElement() ?? "" - } -} diff --git a/VocabularyTrainer/NewLanguage/NewLanguageViewController.swift b/VocabularyTrainer/NewLanguage/NewLanguageViewController.swift index 7342df1..5b1177d 100644 --- a/VocabularyTrainer/NewLanguage/NewLanguageViewController.swift +++ b/VocabularyTrainer/NewLanguage/NewLanguageViewController.swift @@ -38,7 +38,6 @@ final class NewLanguageViewController: UIViewController { accessibilityTrait: .header) private lazy var textField: UITextField = { let textField = UITextField() - textField.translatesAutoresizingMaskIntoConstraints = false textField.placeholder = Localizable.whichLanguage.localize() textField.font = .preferredFont(forTextStyle: .body) textField.backgroundColor = .systemBackground @@ -47,6 +46,8 @@ final class NewLanguageViewController: UIViewController { width: 16, height: textField.frame.height)) textField.leftViewMode = .always + textField.returnKeyType = .done + textField.delegate = self textField.addTarget(self, action: #selector(textFieldEvent), for: .allEvents) return textField }() @@ -75,7 +76,7 @@ final class NewLanguageViewController: UIViewController { }() private var hasDuplicates: Bool { - let newLanguage = textField.text ?? "" + let newLanguage: String = textField.text ?? .init() guard let savedLanguages = UserDefaults.standard.array(forKey: UserDefaultKeys.languages) as? [String] else { return false } @@ -84,16 +85,6 @@ final class NewLanguageViewController: UIViewController { .filter { $0.lowercased().elementsEqual(newLanguage.lowercased()) } return !matches.isEmpty } - - private lazy var randomEmojiLabel: UILabel = { - let label = UILabel() - label.text = randomEmoji - label.font = UIFontMetrics(forTextStyle: .title2).scaledFont(for: .systemFont(ofSize: 30)) - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - private let randomEmoji: String = EmojiChooser.choose // MARK: - Init @@ -109,6 +100,7 @@ final class NewLanguageViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() setUpUI() + hideKeyboardWhenTappedAround() } // MARK: - Private Methods @@ -120,8 +112,7 @@ final class NewLanguageViewController: UIViewController { languageLabel, textField, feedbackLabel, - addButton, - randomEmojiLabel]) + addButton]) setUpConstraints() } @@ -137,10 +128,7 @@ final class NewLanguageViewController: UIViewController { titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10), titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10), - randomEmojiLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 24), - randomEmojiLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - languageLabel.topAnchor.constraint(equalTo: randomEmojiLabel.bottomAnchor, constant: 24), + languageLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 24), languageLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24), languageLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24), @@ -177,8 +165,6 @@ final class NewLanguageViewController: UIViewController { } else { UserDefaults.standard.set([newLanguage], forKey: UserDefaultKeys.languages) } - // TODO: set emoji from to be implemented emoji chooser - UserDefaults.standard.setLanguageEmoji(for: newLanguage, emoji: randomEmoji) successHapticFeedback() delegate.updateLanguageTable(language: newLanguage) dismissView() @@ -230,3 +216,11 @@ final class NewLanguageViewController: UIViewController { dismiss(animated: true) } } + +extension NewLanguageViewController: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + UIAccessibility.focusOn(textField) + return true + } +} diff --git a/VocabularyTrainer/Training/View/TrainingEmptyView.swift b/VocabularyTrainer/Training/View/TrainingEmptyView.swift new file mode 100644 index 0000000..4e772b3 --- /dev/null +++ b/VocabularyTrainer/Training/View/TrainingEmptyView.swift @@ -0,0 +1,102 @@ +// +// TrainingEmptyView.swift +// VocabularyTrainer +// +// Created by Mariana Brasil on 30/07/23. +// Copyright © 2023 mic. All rights reserved. +// + +import UIKit + +// MARK: - Delegate +protocol TrainingEmptyViewDelegate: AnyObject { + func tappedBarButton() +} + +final class TrainingEmptyView: UIView { + + typealias Colors = HomeViewModel.Colors + + weak var delegate: TrainingEmptyViewDelegate? + + // MARK: - Private properties + + /// Bar button to drag/close the view + private lazy var barButton: UIButton = { + let button = ModalCloseButton() + button.addTarget(self, + action: #selector(tappedBarButton), + for: .touchUpInside) + return button + }() + + /// Image view showing the sad robot image. + private let imageView: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.contentMode = .scaleAspectFit + view.image = UIImage(named: "SadRobot") + view.clipsToBounds = true + view.isAccessibilityElement = false + return view + }() + + /// Empty label when the user don't have any language added. + private lazy var emptyLabel: UILabel = .createLabel(font: UIFontMetrics(forTextStyle: .body) + .scaledFont(for: .systemFont(ofSize: 14, weight: .regular)), + text: Localizable.emptyWord.localize(), + fontColor: Colors.subtitle, + textAlignment: .center) + + // MARK: - Initializer + + init() { + super.init(frame: .zero) + setupUI() + setupConstraints() + } + + required init?(coder: NSCoder) { nil } + + // MARK: - Setup + + private func setupUI() { + addSubviews([barButton, imageView, emptyLabel]) + startAnimation() + translatesAutoresizingMaskIntoConstraints = false + } + + private func setupConstraints() { + NSLayoutConstraint.activate([ + barButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 16), + barButton.centerXAnchor.constraint(equalTo: self.centerXAnchor), + barButton.heightAnchor.constraint(equalToConstant: 5), + barButton.widthAnchor.constraint(equalToConstant: 40), + + imageView.leadingAnchor.constraint(equalTo: leadingAnchor), + imageView.topAnchor.constraint(equalTo: barButton.bottomAnchor, constant: 36), + imageView.trailingAnchor.constraint(equalTo: trailingAnchor), + imageView.heightAnchor.constraint(equalToConstant: 70), + imageView.widthAnchor.constraint(equalToConstant: 80), + + emptyLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 24), + emptyLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 28), + emptyLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -24), + emptyLabel.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + private func startAnimation() { + UIView.animate(withDuration: 1, animations: { [weak self] in + self?.imageView.frame.origin.y -= 5 + }){ [weak self] _ in + UIView.animateKeyframes(withDuration: 1, delay: 0.1, options: [.autoreverse, .repeat], animations: { + self?.imageView.frame.origin.y += 5 + }) + } + } + + @objc private func tappedBarButton(){ + delegate?.tappedBarButton() + } +} diff --git a/VocabularyTrainer/Training/View/TrainingView.swift b/VocabularyTrainer/Training/View/TrainingView.swift new file mode 100644 index 0000000..0e87c56 --- /dev/null +++ b/VocabularyTrainer/Training/View/TrainingView.swift @@ -0,0 +1,430 @@ +// +// TrainingView.swift +// VocabularyTrainer +// +// Created by Mariana Brasil on 04/02/23. +// Copyright © 2023 mic. All rights reserved. +// + +import UIKit + +// MARK: - Delegate +protocol TrainingViewDelegate: AnyObject { + func tappedBarButton() +} + +// MARK: - Training View + +final class TrainingView: UIView { + + weak var delegate: TrainingViewDelegate? + + // MARK: - Private properties + + private lazy var vocabularies: [String: String]? = [:] + private lazy var vocabulariesProgresses: [String: Float]? = [:] + private lazy var isKeyShown: Bool? = false + private lazy var currentKey: String? = .init() + private lazy var selectedLanguage: String? = .init() + + private lazy var barButton: UIButton = { + let button = ModalCloseButton() + button.addTarget(self, + action: #selector(tappedBarButton), + for: .touchUpInside) + return button + }() + + private lazy var trainingLanguageLabel: UILabel = { + let label: UILabel = .init() + label.font = UIFontMetrics(forTextStyle: .title2) + .scaledFont(for: .systemFont(ofSize: 20, + weight: .semibold)) + return label + }() + + private lazy var wordLabel: UILabel = { + let label: UILabel = .init() + label.font = UIFontMetrics(forTextStyle: .largeTitle) + .scaledFont(for: .systemFont(ofSize: 32, + weight: .bold)) + return label + }() + + private lazy var answerLabel: UILabel = { + let label: UILabel = .init() + label.font = UIFontMetrics(forTextStyle: .headline) + .scaledFont(for: .systemFont(ofSize: 14, + weight: .bold)) + label.text = Localizable.answer.localize() + return label + }() + + private lazy var answerTextField: UITextField = { + let textField = UITextField() + textField.placeholder = Localizable.translation.localize() + textField.font = .preferredFont(forTextStyle: .body) + textField.backgroundColor = .systemBackground + textField.leftView = UIView(frame: .init(x: 0, + y: 0, + width: 16, + height: textField.frame.height)) + textField.leftViewMode = .always + textField.addTarget(self, action: #selector(textFieldEvent), for: .allEvents) + textField.autocapitalizationType = .none + textField.autocorrectionType = .no + textField.returnKeyType = .done + textField.delegate = self + return textField + }() + + private lazy var checkButton: UIButton = { + let button = UIButton(frame: .zero, + primaryAction: .init(handler: { [weak self] _ in + self?.checkButtonAction() + })) + button.backgroundColor = .systemGray2 + button.layer.cornerRadius = 3 + button.isEnabled = false + button.accessibilityTraits = .button + button.setTitle(Localizable.check.localize(), for: .normal) + button.titleLabel?.font = .preferredFont(forTextStyle: .headline) + return button + }() + + private lazy var skipButton: UIButton = { + let button = UIButton(frame: .zero, + primaryAction: .init(handler: { [weak self] _ in + self?.skipButtonAction() + })) + button.layer.cornerRadius = 3 + button.layer.borderColor = UIColor(named: "grayButton")?.cgColor + button.layer.borderWidth = 1 + button.accessibilityTraits = .button + button.setTitle(Localizable.skip.localize(), for: .normal) + button.titleLabel?.font = .preferredFont(forTextStyle: .caption1) + button.setTitleColor(UIColor(named: "grayButton"), for: .normal) + return button + }() + + private lazy var takeLookButton: UIButton = { + let button = UIButton(frame: .zero, primaryAction: .init(handler: { [weak self] _ in + self?.takeLookAccessibilityAction() + })) + let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(takeLookButtonAction)) + button.addGestureRecognizer(longPressRecognizer) + longPressRecognizer.minimumPressDuration = 0.05 + button.layer.cornerRadius = 3 + button.backgroundColor = .darkGray + button.accessibilityTraits = .button + button.setTitle(Localizable.takeLook.localize(), for: .normal) + button.titleLabel?.font = .preferredFont(forTextStyle: .caption1) + button.setTitleColor(.white, for: .normal) + return button + }() + + // MARK: - Initializer + + init(selectedLanguage: String) { + super.init(frame: .zero) + setUpUI() + setUpConstraints() + setUpLanguageTrained(selectedLanguage: selectedLanguage) + setUpTraining() + } + + required init?(coder: NSCoder) { nil } + + // MARK: - Private Methods + + private func setUpUI() { + backgroundColor = UIColor(named: "background") + addSubviews([barButton, + trainingLanguageLabel, + wordLabel, + answerLabel, + answerTextField, + checkButton, + skipButton, + takeLookButton]) + } + + private func setUpConstraints() { + NSLayoutConstraint.activate([ + barButton.topAnchor.constraint(equalTo: self.topAnchor, constant: 16), + barButton.centerXAnchor.constraint(equalTo: self.centerXAnchor), + barButton.heightAnchor.constraint(equalToConstant: 5), + barButton.widthAnchor.constraint(equalToConstant: 40), + + trainingLanguageLabel.topAnchor.constraint(equalTo: barButton.bottomAnchor, constant: 36), + trainingLanguageLabel.centerXAnchor.constraint(equalTo: barButton.centerXAnchor), + + wordLabel.topAnchor.constraint(equalTo: trainingLanguageLabel.bottomAnchor, constant: 32), + wordLabel.centerXAnchor.constraint(equalTo: barButton.centerXAnchor), + + answerLabel.topAnchor.constraint(equalTo: wordLabel.bottomAnchor, constant: 20), + answerLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 24), + + answerTextField.topAnchor.constraint(equalTo: answerLabel.bottomAnchor, constant: 4), + answerTextField.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -24), + answerTextField.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 24), + answerTextField.heightAnchor.constraint(equalToConstant: 46), + + checkButton.topAnchor.constraint(equalTo: answerTextField.bottomAnchor, constant: 16), + checkButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -24), + checkButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 24), + checkButton.heightAnchor.constraint(equalToConstant: 42), + + skipButton.topAnchor.constraint(equalTo: checkButton.bottomAnchor, constant: 16), + skipButton.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 24), + skipButton.heightAnchor.constraint(equalToConstant: 31), + skipButton.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 2 - 28), + + takeLookButton.topAnchor.constraint(equalTo: checkButton.bottomAnchor, constant: 16), + takeLookButton.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -24), + takeLookButton.heightAnchor.constraint(equalToConstant: 31), + takeLookButton.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width / 2 - 28), + ]) + } + + private func setUpLanguageTrained(selectedLanguage: String) { + let defaultLabel: String = NSLocalizedString("Training", comment: "Training") + trainingLanguageLabel.text = "\(defaultLabel) \(selectedLanguage)" + + guard let vocabs = UserDefaults.standard.dictionary(forKey: selectedLanguage) as? [String: String], + let progresses = UserDefaults.standard.dictionary(forKey: "\(selectedLanguage)Progress") as? [String: Float]? else { + return + } + + vocabularies = vocabs + vocabulariesProgresses = progresses + self.selectedLanguage = selectedLanguage + } + + private func setUpTraining() { + setUpResetTextField() + setUpDisabledCheckButton() + + guard let vocabs = vocabularies, + let progresses = vocabulariesProgresses else { return } + + let totalProgress = getTotalProgressFrom(progresses) + + currentKey = pickRandomKeyFrom(vocabs, withProgresses: progresses, totalProgress: totalProgress) + + guard let key = currentKey else { return } + + if (Int.random(in: 0...1) == 0) { + wordLabel.text = key + isKeyShown = true + } else { + wordLabel.text = vocabs[key] + isKeyShown = false + } + } + + private func getTotalProgressFrom(_ vocabulariesProgresses: [String: Float]) -> Float { + var result = Float(0) + for progress in vocabulariesProgresses { + result += progress.value + } + return result + } + + private func pickRandomKeyFrom(_ vocabularies: [String: String], + withProgresses vocabulariesProgresses: [String: Float], + totalProgress: Float) -> String { + let randomThreshold = Float.random(in: 0...totalProgress) + var summedUpProgresses = Float(0) + var resultKey: String = .init() + + for (key, value) in vocabulariesProgresses { + summedUpProgresses += value + if summedUpProgresses > randomThreshold { + resultKey = key + break + } + } + return resultKey + } + + private func changeWordsProbability(increase: Bool, key: String) { + guard var progresses = vocabulariesProgresses, + let key = currentKey, + let progress = progresses[key] else { return } + + if increase { + progresses[key] = progress+Float(10.0) + } else if (progress-Float(3.0) > 0) { + progresses[key] = progress-Float(3.0) + } else { + progresses[key] = 1.0 + } + UserDefaults.standard.set(progresses, forKey: "\(selectedLanguage)Progress") + } + + private func setUpResetTextField() { + answerTextField.text = .init() + answerTextField.borderStyle = .none + answerTextField.layer.borderWidth = 0 + } + + private func setUpDisabledCheckButton() { + checkButton.backgroundColor = .systemGray2 + checkButton.isEnabled = false + checkButton.accessibilityTraits.insert(.notEnabled) + checkButton.setTitle(Localizable.check.localize(), for: .normal) + } + + private func setUpEnabledCheckButton() { + checkButton.backgroundColor = UIColor(named: "greenButton") + checkButton.isEnabled = true + checkButton.accessibilityTraits.remove(.notEnabled) + checkButton.setTitle(Localizable.check.localize(), for: .normal) + } + + private func takeLookAccessibilityAction() { + guard let isKey = isKeyShown, + let key = currentKey, + let vocabs = vocabularies else { return } + + guard let solution = isKey ? vocabs[key] : key else { return } + + if UIAccessibility.isVoiceOverRunning { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + let answer = "\(Localizable.answer.localize()) \(solution)" + UIAccessibility.post(notification: .announcement, argument: answer) + } + } + } + + @objc + private func tappedBarButton() { + delegate?.tappedBarButton() + } + + @objc + private func textFieldEvent() { + guard let text = answerTextField.text else { return } + + if text.isEmptyOrWhitespace() { + setUpDisabledCheckButton() + } else { + setUpEnabledCheckButton() + } + answerTextField.borderStyle = .none + answerTextField.layer.borderWidth = 0 + } + + @objc + private func checkButtonAction() { + guard let usersAnswer = answerTextField.text, + let isKey = isKeyShown, + let key = currentKey, + let vocabs = vocabularies else { return } + + let solution: String? = isKey ? vocabs[key] : key + + if checkButton.titleLabel?.text != Localizable.check.localize() { + setUpTraining() + UIAccessibility.focusOn(wordLabel) + return + } + + if !usersAnswer.isEmpty, + usersAnswer.uppercased() == solution?.uppercased() { + rightAnswer(solution: solution, key: key) + } else { + wrongAnswer(key: key) + } + } + + @objc + private func skipButtonAction() { + softHapticFeedback() + UIView.animate(withDuration: 0.2, animations: { [weak self] in + self?.skipButton.backgroundColor = .systemGray4 + }, completion: { _ in + self.setUpTraining() + }) + + UIView.animate(withDuration: 0.3, animations: { [weak self] in + self?.skipButton.backgroundColor = nil + }) + + if UIAccessibility.isVoiceOverRunning { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + UIAccessibility.post(notification: .announcement, argument: self.wordLabel.text?.description) + } + } + } + + @objc + private func takeLookButtonAction(_ gestureRecognizer: UILongPressGestureRecognizer) { + if gestureRecognizer.state == .began { + softHapticFeedback() + takeLookButton.backgroundColor = .systemGray2 + guard let isKey = isKeyShown, + let key = currentKey, + let vocabs = vocabularies else { return } + let solution: String? = isKey ? vocabs[key] : key + answerTextField.text = solution + } else if gestureRecognizer.state == .ended { + answerTextField.text = nil + takeLookButton.backgroundColor = .darkGray + } + } + + private func rightAnswer(solution: String?, key: String) { + guard let solution = solution else { return } + changeWordsProbability(increase: false, key: key) + + UIView.animate(withDuration: 0.2, animations: { [weak self] in + self?.answerTextField.layer.borderColor = UIColor(named: "greenButton")?.cgColor + self?.answerTextField.layer.borderWidth = 1 + }, completion: { _ in + self.successHapticFeedback() + }) + checkButton.setTitle(Localizable.nextWord.localize(), for: .normal) + answerTextField.text = solution + } + + private func wrongAnswer(key: String) { + changeWordsProbability(increase: true, key: key) + UIView.animate(withDuration: 0.2, animations: { [weak self] in + self?.answerTextField.layer.borderColor = UIColor(named: "red")?.cgColor + self?.answerTextField.layer.borderWidth = 1 + }) + checkButton.shake() + errorHapticFeedback() + + if UIAccessibility.isVoiceOverRunning { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + UIAccessibility.post(notification: .announcement, argument: Localizable.wrongAnswer.localize()) + } + } + } + + private func successHapticFeedback() { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.success) + } + + private func errorHapticFeedback() { + let generator = UINotificationFeedbackGenerator() + generator.notificationOccurred(.error) + } + + private func softHapticFeedback() { + let generator = UIImpactFeedbackGenerator(style: .soft) + generator.impactOccurred(intensity: 0.70) + } +} + +extension TrainingView: UITextFieldDelegate { + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + UIAccessibility.focusOn(answerTextField) + return true + } +} diff --git a/VocabularyTrainer/Training/ViewController/TrainingViewController.swift b/VocabularyTrainer/Training/ViewController/TrainingViewController.swift new file mode 100644 index 0000000..3479a73 --- /dev/null +++ b/VocabularyTrainer/Training/ViewController/TrainingViewController.swift @@ -0,0 +1,77 @@ +// +// TrainingViewController.swift +// VocabularyTrainer +// +// Created by Mariana Brasil on 04/02/23. +// Copyright © 2023 mic. All rights reserved. +// + +import UIKit + +final class TrainingViewController: UIViewController { + + // MARK: - Public properties + + weak var coordinator: MainCoordinator? + + // MARK: - Private properties + + private let selectedLanguage: String + + private lazy var trainingView: TrainingView = .init(selectedLanguage: selectedLanguage) + + private lazy var emptyView: TrainingEmptyView = .init() + + private var isWordsEmpty: Bool { + return UserDefaults.standard.dictionary(forKey: selectedLanguage) == nil + } + + // MARK: - Init + + init(with language: String) { + self.selectedLanguage = language + super.init(nibName: nil, bundle: nil) + setUpUI() + hideKeyboardWhenTappedAround() + } + + required init?(coder: NSCoder) { nil } + + // MARK: - Private Methods + + private func setUpUI() { + view.backgroundColor = .systemBackground + isWordsEmpty ? setUpEmptyView() : setUpSuccessView() + } + + private func setUpSuccessView() { + view.addSubview(trainingView) + trainingView.delegate = self + trainingView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + trainingView.topAnchor.constraint(equalTo: view.topAnchor), + trainingView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + trainingView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + trainingView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func setUpEmptyView() { + view.addSubview(emptyView) + emptyView.delegate = self + emptyView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: view.topAnchor), + emptyView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + emptyView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + } +} + +extension TrainingViewController: TrainingViewDelegate, TrainingEmptyViewDelegate { + func tappedBarButton() { + dismiss(animated: true) + } +}