From 53ee380c837f2671a4fd5993d85be295e13ddca4 Mon Sep 17 00:00:00 2001 From: Kiryl Kaveryn Date: Sun, 4 Feb 2024 12:26:29 +0400 Subject: [PATCH] [ios] feature: new about screen Signed-off-by: Kiryl Kaveryn fix 1 Signed-off-by: Kiryl Kaveryn --- .../Classes/CustomAlert/Toast/Toast.swift | 12 +- iphone/Maps/Core/Theme/GlobalStyleSheet.swift | 6 + iphone/Maps/Maps.xcodeproj/project.pbxproj | 68 ++- iphone/Maps/UI/Help/AboutController.swift | 325 ----------- .../AboutController/AboutController.swift | 537 ++++++++++++++++++ .../AboutController/Models/AboutInfo.swift | 71 +++ .../AboutController/Models/SocialMedia.swift | 62 ++ .../Views/ButtonsStackView.swift | 56 ++ .../AboutController/Views/DonationView.swift | 67 +++ .../Views/InfoTableViewCell.swift | 33 ++ .../Help/AboutController/Views/InfoView.swift | 81 +++ .../Help/AboutController/Views/OSMView.swift | 87 +++ .../Views/SocialMediaCollectionViewCell.swift | 45 ++ .../SocialMediaCollectionViewHeader.swift | 30 + .../PlacePageInfoViewController.swift | 6 +- 15 files changed, 1148 insertions(+), 338 deletions(-) delete mode 100644 iphone/Maps/UI/Help/AboutController.swift create mode 100644 iphone/Maps/UI/Help/AboutController/AboutController.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Models/AboutInfo.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Models/SocialMedia.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Views/ButtonsStackView.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Views/DonationView.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Views/InfoTableViewCell.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Views/InfoView.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Views/OSMView.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Views/SocialMediaCollectionViewCell.swift create mode 100644 iphone/Maps/UI/Help/AboutController/Views/SocialMediaCollectionViewHeader.swift diff --git a/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift b/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift index 641deca7d3..7e0bfba4a7 100644 --- a/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift +++ b/iphone/Maps/Classes/CustomAlert/Toast/Toast.swift @@ -39,11 +39,11 @@ final class Toast: NSObject { show(in: UIApplication.shared.keyWindow, alignment: .bottom) } - @objc func show(withAlignment alignment: Alignment) { - show(in: UIApplication.shared.keyWindow, alignment: alignment) + @objc func show(withAlignment alignment: Alignment, pinToSafeArea: Bool = true) { + show(in: UIApplication.shared.keyWindow, alignment: alignment, pinToSafeArea: pinToSafeArea) } - @objc func show(in view: UIView?, alignment: Alignment) { + @objc func show(in view: UIView?, alignment: Alignment, pinToSafeArea: Bool = true) { guard let view = view else { return } blurView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(blurView) @@ -58,14 +58,14 @@ final class Toast: NSObject { let topConstraint: NSLayoutConstraint if alignment == .bottom { - topConstraint = blurView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -63) + topConstraint = blurView.bottomAnchor.constraint(equalTo: pinToSafeArea ? view.safeAreaLayoutGuide.bottomAnchor : view.bottomAnchor, constant: -63) } else { - topConstraint = blurView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 50) + topConstraint = blurView.topAnchor.constraint(equalTo: pinToSafeArea ? view.safeAreaLayoutGuide.topAnchor : view.topAnchor, constant: 50) } NSLayoutConstraint.activate([ topConstraint, - blurView.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor) + blurView.centerXAnchor.constraint(equalTo: pinToSafeArea ? view.safeAreaLayoutGuide.centerXAnchor : view.centerXAnchor) ]) UIView.animate(withDuration: kDefaultAnimationDuration) { diff --git a/iphone/Maps/Core/Theme/GlobalStyleSheet.swift b/iphone/Maps/Core/Theme/GlobalStyleSheet.swift index 499c617c20..9b2515a7f1 100644 --- a/iphone/Maps/Core/Theme/GlobalStyleSheet.swift +++ b/iphone/Maps/Core/Theme/GlobalStyleSheet.swift @@ -241,6 +241,12 @@ class GlobalStyleSheet: IStyleSheet { s.fontColorHighlighted = colors.linkBlueHighlighted } + theme.add(styleName: "FlatPrimaryTransButton") { (s) -> (Void) in + s.fontColor = colors.blackPrimaryText + s.backgroundColor = colors.clear + s.fontColorHighlighted = colors.linkBlueHighlighted + } + theme.add(styleName: "FlatRedTransButton") { (s) -> (Void) in s.font = fonts.medium14 s.fontColor = colors.red diff --git a/iphone/Maps/Maps.xcodeproj/project.pbxproj b/iphone/Maps/Maps.xcodeproj/project.pbxproj index 76005f3d07..2128cab789 100644 --- a/iphone/Maps/Maps.xcodeproj/project.pbxproj +++ b/iphone/Maps/Maps.xcodeproj/project.pbxproj @@ -463,10 +463,20 @@ CDCA27842245090900167D87 /* ListenerContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA27832245090900167D87 /* ListenerContainer.swift */; }; CDCA278622451F5000167D87 /* RouteInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCA278522451F5000167D87 /* RouteInfo.swift */; }; CDCA278E2248F34C00167D87 /* MWMRoutingManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = CDCA278B2248F34C00167D87 /* MWMRoutingManager.mm */; }; + ED1080A72B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1080A62B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift */; }; ED1263AB2B6F99F900AD99F3 /* UIView+AddSeparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1263AA2B6F99F900AD99F3 /* UIView+AddSeparator.swift */; }; ED3EAC202B03C88100220A4A /* BottomTabBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3EAC1F2B03C88100220A4A /* BottomTabBarButton.swift */; }; EDBD68072B625724005DD151 /* LocationServicesDisabledAlert.xib in Resources */ = {isa = PBXBuildFile; fileRef = EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */; }; EDBD680B2B62572E005DD151 /* LocationServicesDisabledAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */; }; + EDE243DD2B6D2E640057369B /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243D52B6CF3980057369B /* AboutController.swift */; }; + EDE243E52B6D3F400057369B /* OSMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243E42B6D3F400057369B /* OSMView.swift */; }; + EDE243E72B6D55610057369B /* InfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDE243E02B6D3EA00057369B /* InfoView.swift */; }; + EDFDFB462B7139490013A44C /* AboutInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB452B7139490013A44C /* AboutInfo.swift */; }; + EDFDFB482B7139670013A44C /* SocialMedia.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB472B7139670013A44C /* SocialMedia.swift */; }; + EDFDFB4A2B722A310013A44C /* SocialMediaCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB492B722A310013A44C /* SocialMediaCollectionViewCell.swift */; }; + EDFDFB4C2B722C9C0013A44C /* InfoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB4B2B722C9C0013A44C /* InfoTableViewCell.swift */; }; + EDFDFB522B726F1A0013A44C /* ButtonsStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB512B726F1A0013A44C /* ButtonsStackView.swift */; }; + EDFDFB612B74E2500013A44C /* DonationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDFDFB602B74E2500013A44C /* DonationView.swift */; }; EDC3573B2B7B5029001AE9CA /* CALayer+SetCorner.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDC3573A2B7B5029001AE9CA /* CALayer+SetCorner.swift */; }; F607C1881C032A8800B53A87 /* resources-hdpi_clear in Resources */ = {isa = PBXBuildFile; fileRef = F607C1831C032A8800B53A87 /* resources-hdpi_clear */; }; F607C18A1C032A8800B53A87 /* resources-hdpi_dark in Resources */ = {isa = PBXBuildFile; fileRef = F607C1841C032A8800B53A87 /* resources-hdpi_dark */; }; @@ -626,7 +636,6 @@ FA853BEF26BC5BA40026D455 /* libdescriptions.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FA853BEE26BC5BA40026D455 /* libdescriptions.a */; }; FA853BF326BC5DE50026D455 /* libshaders.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FA853BF226BC5DE50026D455 /* libshaders.a */; }; FA85D43D27958BF500B858E9 /* FaqController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA85D43C27958BF500B858E9 /* FaqController.swift */; }; - FA85D43F2795969700B858E9 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA85D43E2795969700B858E9 /* AboutController.swift */; }; FA85D44E279B738F00B858E9 /* CopyableLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA85D44D279B738F00B858E9 /* CopyableLabel.swift */; }; FA8E808925F412E2002A1434 /* FirstSession.mm in Sources */ = {isa = PBXBuildFile; fileRef = FA8E808825F412E2002A1434 /* FirstSession.mm */; }; FAF9DDA32A86DC54000D7037 /* libharfbuzz.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FAF9DDA22A86DC54000D7037 /* libharfbuzz.a */; }; @@ -1330,12 +1339,22 @@ CDCA278C2248F34C00167D87 /* MWMRouterResultCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MWMRouterResultCode.h; sourceTree = ""; }; CDCA278F2248F3B800167D87 /* MWMLocationModeListener.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMLocationModeListener.h; sourceTree = ""; }; CDE0F3AD225B8D45008BA5C3 /* MWMSpeedCameraManagerMode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MWMSpeedCameraManagerMode.h; sourceTree = ""; }; + ED1080A62B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialMediaCollectionViewHeader.swift; sourceTree = ""; }; ED1263AA2B6F99F900AD99F3 /* UIView+AddSeparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+AddSeparator.swift"; sourceTree = ""; }; ED3EAC1F2B03C88100220A4A /* BottomTabBarButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomTabBarButton.swift; sourceTree = ""; }; ED48BBB817C2B1E2003E7E92 /* CircleView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CircleView.h; sourceTree = ""; }; ED48BBB917C2B1E2003E7E92 /* CircleView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CircleView.m; sourceTree = ""; }; EDBD68062B625724005DD151 /* LocationServicesDisabledAlert.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LocationServicesDisabledAlert.xib; sourceTree = ""; }; EDBD680A2B62572E005DD151 /* LocationServicesDisabledAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationServicesDisabledAlert.swift; sourceTree = ""; }; + EDE243D52B6CF3980057369B /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; + EDE243E02B6D3EA00057369B /* InfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoView.swift; sourceTree = ""; }; + EDE243E42B6D3F400057369B /* OSMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSMView.swift; sourceTree = ""; }; + EDFDFB452B7139490013A44C /* AboutInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutInfo.swift; sourceTree = ""; }; + EDFDFB472B7139670013A44C /* SocialMedia.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialMedia.swift; sourceTree = ""; }; + EDFDFB492B722A310013A44C /* SocialMediaCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialMediaCollectionViewCell.swift; sourceTree = ""; }; + EDFDFB4B2B722C9C0013A44C /* InfoTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoTableViewCell.swift; sourceTree = ""; }; + EDFDFB512B726F1A0013A44C /* ButtonsStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsStackView.swift; sourceTree = ""; }; + EDFDFB602B74E2500013A44C /* DonationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DonationView.swift; sourceTree = ""; }; EDC3573A2B7B5029001AE9CA /* CALayer+SetCorner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CALayer+SetCorner.swift"; sourceTree = ""; }; EE026F0511D6AC0D00645242 /* classificator.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = classificator.txt; path = ../../data/classificator.txt; sourceTree = SOURCE_ROOT; }; EE164810135CEE49003B8A3E /* 06_code2000.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = 06_code2000.ttf; path = ../../data/06_code2000.ttf; sourceTree = SOURCE_ROOT; }; @@ -1607,7 +1626,6 @@ FA853BEE26BC5BA40026D455 /* libdescriptions.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libdescriptions.a; sourceTree = BUILT_PRODUCTS_DIR; }; FA853BF226BC5DE50026D455 /* libshaders.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libshaders.a; sourceTree = BUILT_PRODUCTS_DIR; }; FA85D43C27958BF500B858E9 /* FaqController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaqController.swift; sourceTree = ""; }; - FA85D43E2795969700B858E9 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; FA85D44D279B738F00B858E9 /* CopyableLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLabel.swift; sourceTree = ""; }; FA85F632145DDDC20090E1A0 /* packed_polygons.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; name = packed_polygons.bin; path = ../../data/packed_polygons.bin; sourceTree = SOURCE_ROOT; }; FA8E808825F412E2002A1434 /* FirstSession.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = FirstSession.mm; sourceTree = ""; }; @@ -2918,6 +2936,39 @@ path = Location; sourceTree = ""; }; + EDFDFB412B7108090013A44C /* AboutController */ = { + isa = PBXGroup; + children = ( + EDFDFB442B7139380013A44C /* Models */, + EDFDFB552B72821D0013A44C /* Views */, + EDE243D52B6CF3980057369B /* AboutController.swift */, + ); + path = AboutController; + sourceTree = ""; + }; + EDFDFB442B7139380013A44C /* Models */ = { + isa = PBXGroup; + children = ( + EDFDFB452B7139490013A44C /* AboutInfo.swift */, + EDFDFB472B7139670013A44C /* SocialMedia.swift */, + ); + path = Models; + sourceTree = ""; + }; + EDFDFB552B72821D0013A44C /* Views */ = { + isa = PBXGroup; + children = ( + EDE243E42B6D3F400057369B /* OSMView.swift */, + EDFDFB602B74E2500013A44C /* DonationView.swift */, + EDE243E02B6D3EA00057369B /* InfoView.swift */, + EDFDFB492B722A310013A44C /* SocialMediaCollectionViewCell.swift */, + ED1080A62B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift */, + EDFDFB512B726F1A0013A44C /* ButtonsStackView.swift */, + EDFDFB4B2B722C9C0013A44C /* InfoTableViewCell.swift */, + ); + path = Views; + sourceTree = ""; + }; F607C18B1C047FCA00B53A87 /* Segue */ = { isa = PBXGroup; children = ( @@ -3578,8 +3629,8 @@ FA85D4372795895500B858E9 /* Help */ = { isa = PBXGroup; children = ( + EDFDFB412B7108090013A44C /* AboutController */, FA85D43C27958BF500B858E9 /* FaqController.swift */, - FA85D43E2795969700B858E9 /* AboutController.swift */, ); path = Help; sourceTree = ""; @@ -4005,6 +4056,7 @@ 473CBF9B2164DD470059BD54 /* SettingsTableViewSelectableProgressCell.swift in Sources */, 47E3C72D2111E6A2008B3B27 /* FadeTransitioning.swift in Sources */, 34845DAF1E1649F6003D55B9 /* DownloaderNoResultsEmbedViewController.swift in Sources */, + EDFDFB482B7139670013A44C /* SocialMedia.swift in Sources */, 993DF0B523F6B2EF00AC231A /* PlacePageElevationLayout.swift in Sources */, 44360A0D2A7D34990016F412 /* TransportRuler.swift in Sources */, CD6E8677226774C700D1EDF7 /* CPConstants.swift in Sources */, @@ -4023,6 +4075,7 @@ 3454D7D41E07F045004AF2AD /* UIImageView+Coloring.m in Sources */, 993DF11D23F6BDB100AC231A /* UIToolbarRenderer.swift in Sources */, 99A906E923F6F7030005872B /* WikiDescriptionViewController.swift in Sources */, + EDFDFB522B726F1A0013A44C /* ButtonsStackView.swift in Sources */, 993DF11023F6BDB100AC231A /* MWMButtonRenderer.swift in Sources */, 3463BA671DE81DB90082417F /* MWMTrafficButtonViewController.mm in Sources */, 993DF10323F6BDB100AC231A /* MainTheme.swift in Sources */, @@ -4056,6 +4109,7 @@ 47CA68D4250043C000671019 /* BookmarksListPresenter.swift in Sources */, F6E2FF451E097BA00083EBEC /* SettingsTableViewLinkCell.swift in Sources */, 34C9BD0A1C6DBCDA000DC38D /* MWMNavigationController.m in Sources */, + ED1080A72B791CFE0023F27E /* SocialMediaCollectionViewHeader.swift in Sources */, F6E2FE311E097BA00083EBEC /* MWMStreetEditorViewController.mm in Sources */, F6E2FE281E097BA00083EBEC /* MWMOpeningHoursSection.mm in Sources */, 3406FA161C6E0C3300E9FAD2 /* MWMMapDownloadDialog.mm in Sources */, @@ -4074,6 +4128,7 @@ 349D1CE41E3F836900A878FD /* UIViewController+Hierarchy.swift in Sources */, CDB4D4E1222D70DF00104869 /* CarPlayMapViewController.swift in Sources */, 471AB98923AA8A3500F56D49 /* IDownloaderDataSource.swift in Sources */, + EDE243E72B6D55610057369B /* InfoView.swift in Sources */, F692F3831EA0FAF5001E82EB /* MWMAutoupdateController.mm in Sources */, 34BF0CC71C31304A00D097EB /* MWMAuthorizationCommon.mm in Sources */, 34AB664D1FC5AA330078E451 /* RouteManagerFooterView.swift in Sources */, @@ -4096,7 +4151,6 @@ CDB4D5002231412900104869 /* MapTemplateBuilder.swift in Sources */, 34AB66171FC5AA320078E451 /* MWMiPhoneRoutePreview.m in Sources */, 99A906EA23F6F7030005872B /* PlacePageInfoViewController.swift in Sources */, - FA85D43F2795969700B858E9 /* AboutController.swift in Sources */, 993DF11723F6BDB100AC231A /* UINavigationBarRenderer.swift in Sources */, 6741A9E71BF340DE002C974C /* MWMCircularProgressView.m in Sources */, 34AC8FDB1EFC07FE00E7F910 /* UILabel+NumberOfVisibleLines.swift in Sources */, @@ -4192,6 +4246,7 @@ 3486B5191E27AD3B0069C126 /* MWMFrameworkListener.mm in Sources */, 3404756B1E081A4600C92850 /* MWMSearch+CoreSpotlight.mm in Sources */, CD9AD96C2281B56900EC174A /* CPViewPortState.swift in Sources */, + EDE243DD2B6D2E640057369B /* AboutController.swift in Sources */, 3404755C1E081A4600C92850 /* MWMLocationManager.mm in Sources */, 3454D7BC1E07F045004AF2AD /* CLLocation+Mercator.mm in Sources */, 47E3C7272111E5A8008B3B27 /* AlertPresentationController.swift in Sources */, @@ -4200,6 +4255,7 @@ 6741AA0B1BF340DE002C974C /* MWMMapViewControlsManager.mm in Sources */, F6E2FED91E097BA00083EBEC /* MWMSearchContentView.m in Sources */, 47CA68F5250B550C00671019 /* TrackCell.swift in Sources */, + EDFDFB4C2B722C9C0013A44C /* InfoTableViewCell.swift in Sources */, 47CA68F8250F8AB700671019 /* BookmarksListSectionHeader.swift in Sources */, F6BD1D211CA412920047B8E8 /* MWMOsmAuthAlert.mm in Sources */, 47CF2E6323BA0DD500D11C30 /* CopyLabel.swift in Sources */, @@ -4271,6 +4327,7 @@ 674A7E301C0DB10B003D48E1 /* MWMMapWidgets.mm in Sources */, 34AB66291FC5AA330078E451 /* RouteManagerViewController.swift in Sources */, 3404754D1E081A4600C92850 /* MWMKeyboard.m in Sources */, + EDE243E52B6D3F400057369B /* OSMView.swift in Sources */, 993DF10C23F6BDB100AC231A /* MWMTableViewCellRenderer.swift in Sources */, 3457C4261F680F1900028233 /* String+BoundingRect.swift in Sources */, 34EF94291C05A6F30050B714 /* MWMSegue.m in Sources */, @@ -4282,6 +4339,7 @@ 34AB66891FC5AA330078E451 /* NavigationControlView.swift in Sources */, 479EE94A2292FB03009DEBA6 /* ActivityIndicator.swift in Sources */, ED3EAC202B03C88100220A4A /* BottomTabBarButton.swift in Sources */, + EDFDFB612B74E2500013A44C /* DonationView.swift in Sources */, 47B9065321C7FA400079C85E /* MWMImageCache.m in Sources */, F6FEA82E1C58F108007223CC /* MWMButton.m in Sources */, 34B924431DC8A29C0008D971 /* MWMMailViewController.m in Sources */, @@ -4297,8 +4355,10 @@ F6E2FF571E097BA00083EBEC /* MWMMobileInternetViewController.m in Sources */, 993DF11323F6BDB100AC231A /* UITableViewRenderer.swift in Sources */, 34AB66261FC5AA330078E451 /* RouteManagerDimView.swift in Sources */, + EDFDFB4A2B722A310013A44C /* SocialMediaCollectionViewCell.swift in Sources */, 6741AA2B1BF340DE002C974C /* CircleView.m in Sources */, 4788739220EE326500F6826B /* VerticallyAlignedButton.swift in Sources */, + EDFDFB462B7139490013A44C /* AboutInfo.swift in Sources */, 3444DFDE1F18A5AF00E73099 /* SideButtonsArea.swift in Sources */, CDCA278622451F5000167D87 /* RouteInfo.swift in Sources */, 3467CEB6202C6FA900D3C670 /* BMCNotificationsCell.swift in Sources */, diff --git a/iphone/Maps/UI/Help/AboutController.swift b/iphone/Maps/UI/Help/AboutController.swift deleted file mode 100644 index 752a212147..0000000000 --- a/iphone/Maps/UI/Help/AboutController.swift +++ /dev/null @@ -1,325 +0,0 @@ -import Foundation - -final class AboutController: MWMViewController, UITableViewDataSource, UITableViewDelegate { - - // Returns a human-readable maps data version. - static private func formattedMapsDataVersion() -> String { - // First, convert version code like 220131 to a date. - let df = DateFormatter() - df.locale = Locale(identifier:"en_US_POSIX") - df.dateFormat = "yyMMdd" - let mapsVersionInt = FrameworkHelper.dataVersion() - let mapsDate = df.date(from: String(mapsVersionInt))! - // Second, print the date in the local user's format. - df.locale = Locale.current - df.dateStyle = .long - df.timeStyle = .none - return String(format: L("data_version"), df.string(from:mapsDate), mapsVersionInt) - } - - private var onDidAppearCompletionHandler: (() -> Void)? - - init(onDidAppearCompletionHandler: (() -> Void)? = nil) { - self.onDidAppearCompletionHandler = onDidAppearCompletionHandler - super.init(nibName: nil, bundle: nil) - } - - @available(*, unavailable) - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - private func isDonateEnabled() -> Bool { - return Settings.donateUrl() != nil - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if let completionHandler = onDidAppearCompletionHandler { - completionHandler() - onDidAppearCompletionHandler = nil - } - } - - override func loadView() { - super.loadView() - - if isDonateEnabled() { - labels[0][kDonateCellIndex] = "donate" - } - - title = L("about_menu_title") - - // Menu items. - var tableStyle: UITableView.Style - if #available(iOS 13.0, *) { - tableStyle = .insetGrouped - } else { - tableStyle = .grouped - } - let table = UITableView(frame: CGRect.zero, style: tableStyle) - // Default grey background for table view sections. - table.setValue("TableView:PressBackground", forKey: "styleName") - // For full-width cell separators. - table.separatorInset = .zero - table.translatesAutoresizingMaskIntoConstraints = false - table.dataSource = self - table.delegate = self - table.sectionHeaderHeight = UITableView.automaticDimension - - view.addSubview(table) - NSLayoutConstraint.activate([ - table.leadingAnchor.constraint(equalTo: view.leadingAnchor), - table.trailingAnchor.constraint(equalTo: view.trailingAnchor), - table.topAnchor.constraint(equalTo: view.topAnchor), - table.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - } - - private lazy var header: UIView = { () -> UIView in - // Setup header view with app's and data versions. - let header = UIView() - header.translatesAutoresizingMaskIntoConstraints = false - let kMargin = 20.0 - - // App icon. - // TODO: Reduce memory cache footprint by loading the icon without caching. - let icon = UIImageView(image: UIImage(named: "AppIcon60x60")) - icon.layer.cornerRadius = icon.width / 4 - icon.clipsToBounds = true - icon.translatesAutoresizingMaskIntoConstraints = false - header.addSubview(icon) - NSLayoutConstraint.activate([ - icon.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: kMargin), - icon.topAnchor.constraint(equalTo: header.topAnchor, constant: kMargin), - icon.widthAnchor.constraint(equalTo: icon.heightAnchor) - ]) - - // App version. - let appVersion = CopyableLabel() - appVersion.translatesAutoresizingMaskIntoConstraints = false - appVersion.styleName = "blackPrimaryText" - appVersion.adjustsFontSizeToFitWidth = true - let appInfo = AppInfo.shared(); - // Use strong left-to-right unicode direction characters for the app version. - appVersion.text = String(format: L("version"), "\u{2066}\(appInfo.bundleVersion)-\(appInfo.buildNumber)\u{2069}") - header.addSubview(appVersion) - NSLayoutConstraint.activate([ - appVersion.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: kMargin), - appVersion.topAnchor.constraint(equalTo: header.topAnchor, constant: kMargin), - appVersion.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -kMargin) - ]) - - // Maps data version. - let mapsVersion = CopyableLabel() - mapsVersion.translatesAutoresizingMaskIntoConstraints = false - mapsVersion.styleName = "blackSecondaryText" - mapsVersion.adjustsFontSizeToFitWidth = true - mapsVersion.text = AboutController.formattedMapsDataVersion() - header.addSubview(mapsVersion) - NSLayoutConstraint.activate([ - mapsVersion.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: kMargin), - mapsVersion.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -kMargin), - mapsVersion.topAnchor.constraint(equalTo: appVersion.bottomAnchor, constant: kMargin) - ]) - - // Long description text. - let about = UILabel() - about.translatesAutoresizingMaskIntoConstraints = false - about.styleName = "blackPrimaryText" - about.numberOfLines = 0 - about.text = L("about_description") - header.addSubview(about) - NSLayoutConstraint.activate([ - about.leadingAnchor.constraint(equalTo: header.leadingAnchor, constant: kMargin), - about.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -kMargin), - about.trailingAnchor.constraint(equalTo: header.trailingAnchor, constant: -kMargin), - about.topAnchor.constraint(equalTo: icon.bottomAnchor, constant: kMargin), - about.topAnchor.constraint(equalTo: mapsVersion.bottomAnchor, constant: kMargin) - ]) - return header - }() - - // MARK: - UITableView data source - - // Update didSelect... delegate and tools/python/clean_strings_txt.py after modifying this list. - private var labels = [ - ["news", "faq", "report_a_bug", "how_to_support_us", "rate_the_app"], - ["telegram", "github", "website", "email", "matrix", "mastodon", "facebook", "twitter", "instagram", "openstreetmap"], - ["privacy_policy", "terms_of_use", "copyright"], - ] - // Replaces "how_to_support_us" above. - private let kDonateCellIndex = 3 - - // Additional section is used to properly resize the header view by putting it in the table cell. - func numberOfSections(in tableView: UITableView) -> Int { return labels.count + 1 } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if section == 0 { - return 1 - } - return labels[section - 1].count - } - - private func getCell(tableView: UITableView, identifier: String) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: identifier) else { - return UITableViewCell(style: .default, reuseIdentifier: identifier) - } - return cell - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - var cell: UITableViewCell - if indexPath[0] == 0 { - cell = getCell(tableView: tableView, identifier: "header") - cell.contentView.addSubview(header) - NSLayoutConstraint.activate([ - header.topAnchor.constraint(equalTo: cell.contentView.topAnchor), - header.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), - header.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor), - header.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor) - ]) - } else { - cell = getCell(tableView: tableView, identifier: "default") - let text = L(labels[indexPath[0] - 1][indexPath[1]]) - if (indexPath.section == 1 && indexPath.row == kDonateCellIndex && Settings.isNY()) { - cell.textLabel!.text = "🎄" + text + "🎄" - } else { - cell.textLabel!.text = text - } - } - return cell - } - - // MARK: - UITableView delegate - - private let kiOSEmail = "ios@organicmaps.app" - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - tableView.deselectRow(at: indexPath, animated: true) - // See labels array above. - switch indexPath[0] { - // Header section click. - case 0: self.openUrl(L("translated_om_site_url") + "donate/") - // First buttons section. - case 1: switch indexPath[1] { - case 0: self.openUrl(L("translated_om_site_url") + "news/") - case 1: self.navigationController?.pushViewController(FaqController(), animated: true) - case 2: sendEmailWith(header: "Organic Maps Bugreport", toRecipients: [kiOSEmail]) - case kDonateCellIndex: self.openUrl(isDonateEnabled() ? Settings.donateUrl() : L("translated_om_site_url") + "support-us/") - case 4: UIApplication.shared.rateApp() - default: fatalError("Invalid cell0 \(indexPath)") - } - // Second section. Open urls in external Safari so logged-in users can easily follow us. - case 2: switch indexPath[1] { - case 0: self.openUrl(L("telegram_url"), inSafari: true) - case 1: self.openUrl("https://github.com/organicmaps/organicmaps/", inSafari: true) - case 2: self.openUrl(L("translated_om_site_url")) - case 3: sendEmailWith(header: "Organic Maps", toRecipients: [kiOSEmail]) - case 4: self.openUrl("https://matrix.to/#/#organicmaps:matrix.org", inSafari: true) - case 5: self.openUrl("https://fosstodon.org/@organicmaps", inSafari: true) - case 6: self.openUrl("https://facebook.com/OrganicMaps", inSafari: true) - case 7: self.openUrl("https://twitter.com/OrganicMapsApp", inSafari: true) - case 8: self.openUrl(L("instagram_url"), inSafari: true) - case 9: self.openUrl(L("osm_wiki_about_url"), inSafari: true) - default: fatalError("Invalid cell1 \(indexPath)") - } - // Third section. - case 3: switch indexPath[1] { - case 0: self.openUrl(L("translated_om_site_url") + "privacy/") - case 1: self.openUrl(L("translated_om_site_url") + "terms/") - case 2: showCopyright() - default: fatalError("Invalid cell2 \(indexPath)") - } - default: fatalError("Invalid section \(indexPath[0])") - } - } - - private func showCopyright() { - let path = Bundle.main.path(forResource: "copyright", ofType: "html")! - let html = try! String(contentsOfFile: path, encoding: String.Encoding.utf8) - let webViewController = WebViewController.init(html: html, baseUrl: nil, title: L("copyright"))! - webViewController.openInSafari = true - self.navigationController?.pushViewController(webViewController, animated: true) - } - - private func emailSubject(subject: String) -> String { - let appInfo = AppInfo.shared() - return String(format:"[%@-%@ iOS] %@", appInfo.bundleVersion, appInfo.buildNumber, subject) - } - - private func emailBody() -> String { - let appInfo = AppInfo.shared() - return String(format: "\n\n\n\n- %@ (%@)\n- Organic Maps %@-%@\n- %@-%@\n- %@\n", - appInfo.deviceModel, UIDevice.current.systemVersion, - appInfo.bundleVersion, appInfo.buildNumber, - Locale.current.languageCode ?? "", - Locale.current.regionCode ?? "", - Locale.preferredLanguages.joined(separator: ", ")) - } - - private func openOutlook(subject: String, body: String, recipients: [String]) -> Bool { - var components = URLComponents(string: "ms-outlook://compose")! - components.queryItems = [ - URLQueryItem(name: "to", value: recipients.joined(separator: ";")), - URLQueryItem(name: "subject", value: subject), - URLQueryItem(name: "body", value: body), - ] - - if let url = components.url, UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - return true - } - return false - } - - private func openGmail(subject: String, body: String, recipients: [String]) -> Bool { - var components = URLComponents(string: "googlegmail://co")! - components.queryItems = [ - URLQueryItem(name: "to", value: recipients.joined(separator: ";")), - URLQueryItem(name: "subject", value: subject), - URLQueryItem(name: "body", value: body), - ] - - if let url = components.url, UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - return true - } - return false - } - - private func sendEmailWith(header: String, toRecipients: [String]) { - let subject = emailSubject(subject: header) - let body = emailBody() - - // Before iOS 14, try to open alternate email apps first, assuming that if users installed them, they're using them. - let os = ProcessInfo().operatingSystemVersion - if (os.majorVersion < 14 && (openGmail(subject: subject, body: body, recipients: toRecipients) || - openOutlook(subject: subject, body: body, recipients: toRecipients))) { - return - } - // From iOS 14, it is possible to change the default mail app, and mailto should open a default mail app. - if MWMMailViewController.canSendMail() { - let vc = MWMMailViewController() - vc.mailComposeDelegate = self - vc.setSubject(subject) - vc.setMessageBody(body, isHTML:false) - vc.setToRecipients(toRecipients) - vc.navigationBar.tintColor = UIColor.whitePrimaryText() - self.present(vc, animated: true, completion:nil) - } else { - let text = String(format:L("email_error_body"), toRecipients.joined(separator: ";")) - let alert = UIAlertController(title: L("email_error_title"), message: text, preferredStyle: .alert) - let action = UIAlertAction(title: L("ok"), style: .default, handler: nil) - alert.addAction(action) - present(alert, animated: true, completion: nil) - } - } -} - -// To properly close Email popup. -extension AboutController: MFMailComposeViewControllerDelegate { - func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { - self.dismiss(animated: true, completion: nil) - } -} diff --git a/iphone/Maps/UI/Help/AboutController/AboutController.swift b/iphone/Maps/UI/Help/AboutController/AboutController.swift new file mode 100644 index 0000000000..6ab0665ec3 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/AboutController.swift @@ -0,0 +1,537 @@ +final class AboutController: MWMViewController { + + fileprivate struct AboutInfoTableViewCellModel { + let title: String + let image: UIImage? + let didTapHandler: (() -> Void)? + } + + fileprivate struct SocialMediaCollectionViewCellModel { + let image: UIImage + let didTapHandler: (() -> Void)? + } + + private enum Constants { + static let infoTableViewCellHeight: CGFloat = 40 + static let socialMediaCollectionViewCellMaxWidth: CGFloat = 50 + static let socialMediaCollectionViewSpacing: CGFloat = 25 + static let socialMediaCollectionNumberOfItemsInRowCompact: CGFloat = 5 + static let socialMediaCollectionNumberOfItemsInRowRegular: CGFloat = 10 + } + + private let scrollView = UIScrollView() + private let stackView = UIStackView() + private let logoImageView = UIImageView() + private let headerTitleLabel = UILabel() + private let additionalInfoStackView = UIStackView() + private let donationView = DonationView() + private let osmView = OSMView() + private let infoTableView = UITableView(frame: .zero, style: .plain) + private var infoTableViewHeightAnchor: NSLayoutConstraint? + private let socialMediaHeaderLabel = UILabel() + private let socialMediaCollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) + private lazy var socialMediaCollectionViewHeighConstraint = socialMediaCollectionView.heightAnchor.constraint(equalToConstant: .zero) + private let termsOfUseAndPrivacyPolicyView = ButtonsStackView() + private var infoTableViewData = [AboutInfoTableViewCellModel]() + private var socialMediaCollectionViewData = [SocialMediaCollectionViewCellModel]() + private var onDidAppearCompletionHandler: (() -> Void)? + + init(onDidAppearCompletionHandler: (() -> Void)? = nil) { + self.onDidAppearCompletionHandler = onDidAppearCompletionHandler + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + arrangeViews() + layoutViews() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + updateCollection() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if let completionHandler = onDidAppearCompletionHandler { + completionHandler() + onDidAppearCompletionHandler = nil + } + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + updateCollection() + } +} + +// MARK: - Private +private extension AboutController { + func setupViews() { + func setupTitle() { + let titleView = UILabel() + titleView.text = Self.formattedAppVersion() + titleView.textColor = .white + titleView.font = UIFont.systemFont(ofSize: 17, weight: .semibold) + titleView.isUserInteractionEnabled = true + titleView.numberOfLines = 1 + titleView.allowsDefaultTighteningForTruncation = true + titleView.adjustsFontSizeToFitWidth = true + titleView.minimumScaleFactor = 0.5 + let titleDidTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(appVersionButtonTapped)) + titleView.addGestureRecognizer(titleDidTapGestureRecognizer) + navigationItem.titleView = titleView + } + + func setupScrollAndStack() { + scrollView.delaysContentTouches = false + scrollView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 20, right: 0) + + stackView.axis = .vertical + stackView.distribution = .fill + stackView.alignment = .center + stackView.spacing = 15 + } + + func setupLogo() { + logoImageView.contentMode = .scaleAspectFit + logoImageView.image = UIImage(named: "logo") + } + + func setupHeaderTitle() { + headerTitleLabel.setStyleAndApply("semibold18:blackPrimaryText") + headerTitleLabel.text = L("about_headline") + headerTitleLabel.textAlignment = .center + headerTitleLabel.numberOfLines = 1 + headerTitleLabel.allowsDefaultTighteningForTruncation = true + headerTitleLabel.adjustsFontSizeToFitWidth = true + headerTitleLabel.minimumScaleFactor = 0.5 + } + + func setupAdditionalInfo() { + additionalInfoStackView.axis = .vertical + additionalInfoStackView.spacing = 15 + + [AboutInfo.noTracking, .noWifi, .community].forEach({ additionalInfoStackView.addArrangedSubview(InfoView(image: nil, title: $0.title)) }) + } + + func setupDonation() { + donationView.donateButtonDidTapHandler = { [weak self] in + guard let self else { return } + self.openUrl(self.isDonateEnabled() ? Settings.donateUrl() : L("translated_om_site_url") + "support-us/") + } + } + + func setupOSM() { + osmView.setMapDate(Self.formattedMapsDataVersion()) + osmView.didTapHandler = { [weak self] in + self?.openUrl("https://www.openstreetmap.org/") + } + } + + func setupInfoTable() { + infoTableView.setStyleAndApply("ClearBackground") + infoTableView.delegate = self + infoTableView.dataSource = self + infoTableView.separatorStyle = .none + infoTableView.isScrollEnabled = false + infoTableView.showsVerticalScrollIndicator = false + infoTableView.contentInset = .zero + infoTableView.register(cell: InfoTableViewCell.self) + } + + func setupSocialMediaCollection() { + socialMediaHeaderLabel.setStyleAndApply("regular16:blackPrimaryText") + socialMediaHeaderLabel.text = L("follow_us") + socialMediaHeaderLabel.numberOfLines = 1 + socialMediaHeaderLabel.allowsDefaultTighteningForTruncation = true + socialMediaHeaderLabel.adjustsFontSizeToFitWidth = true + socialMediaHeaderLabel.minimumScaleFactor = 0.5 + + socialMediaCollectionView.backgroundColor = .clear + socialMediaCollectionView.isScrollEnabled = false + socialMediaCollectionView.dataSource = self + socialMediaCollectionView.delegate = self + socialMediaCollectionView.register(cell: SocialMediaCollectionViewCell.self) + } + + func setupTermsAndPrivacy() { + termsOfUseAndPrivacyPolicyView.addButton(title: L("privacy_policy"), didTapHandler: { [weak self] in + self?.openUrl(L("translated_om_site_url") + "privacy/") + }) + termsOfUseAndPrivacyPolicyView.addButton(title: L("terms_of_use"), didTapHandler: { [weak self] in + self?.openUrl(L("translated_om_site_url") + "terms/") + }) + termsOfUseAndPrivacyPolicyView.addButton(title: L("copyright"), didTapHandler: { [weak self] in + self?.showCopyright() + }) + } + + view.setStyleAndApply("PressBackground") + + setupTitle() + setupScrollAndStack() + setupLogo() + setupHeaderTitle() + setupAdditionalInfo() + setupDonation() + setupOSM() + setupInfoTable() + setupSocialMediaCollection() + setupTermsAndPrivacy() + + infoTableViewData = buildInfoTableViewData() + socialMediaCollectionViewData = buildSocialMediaCollectionViewData() + } + + func arrangeViews() { + view.addSubview(scrollView) + scrollView.addSubview(stackView) + stackView.addArrangedSubview(logoImageView) + stackView.addArrangedSubview(headerTitleLabel) + stackView.addArrangedSubviewWithSeparator(additionalInfoStackView) + if isDonateEnabled() { + stackView.addArrangedSubviewWithSeparator(donationView) + } + stackView.addArrangedSubviewWithSeparator(osmView) + stackView.addArrangedSubviewWithSeparator(infoTableView) + stackView.addArrangedSubviewWithSeparator(socialMediaHeaderLabel) + stackView.addArrangedSubview(socialMediaCollectionView) + stackView.addArrangedSubviewWithSeparator(termsOfUseAndPrivacyPolicyView) + } + + func layoutViews() { + scrollView.translatesAutoresizingMaskIntoConstraints = false + stackView.translatesAutoresizingMaskIntoConstraints = false + logoImageView.translatesAutoresizingMaskIntoConstraints = false + additionalInfoStackView.translatesAutoresizingMaskIntoConstraints = false + donationView.translatesAutoresizingMaskIntoConstraints = false + infoTableView.translatesAutoresizingMaskIntoConstraints = false + socialMediaCollectionView.translatesAutoresizingMaskIntoConstraints = false + termsOfUseAndPrivacyPolicyView.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + scrollView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.contentLayoutGuide.widthAnchor.constraint(equalTo: scrollView.frameLayoutGuide.widthAnchor), + + stackView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor, constant: 20), + stackView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor, constant: -20), + stackView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor), + stackView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor), + + logoImageView.heightAnchor.constraint(equalToConstant: 64), + logoImageView.widthAnchor.constraint(equalTo: logoImageView.heightAnchor), + + additionalInfoStackView.widthAnchor.constraint(equalTo: stackView.widthAnchor), + + osmView.widthAnchor.constraint(equalTo: stackView.widthAnchor), + + infoTableView.widthAnchor.constraint(equalTo: stackView.widthAnchor), + infoTableView.heightAnchor.constraint(equalToConstant: Constants.infoTableViewCellHeight * CGFloat(infoTableViewData.count)), + + socialMediaHeaderLabel.leadingAnchor.constraint(equalTo: socialMediaCollectionView.leadingAnchor), + + socialMediaCollectionView.widthAnchor.constraint(equalTo: stackView.widthAnchor), + socialMediaCollectionView.contentLayoutGuide.widthAnchor.constraint(equalTo: stackView.widthAnchor), + socialMediaCollectionViewHeighConstraint, + + termsOfUseAndPrivacyPolicyView.widthAnchor.constraint(equalTo: stackView.widthAnchor), + ]) + donationView.widthAnchor.constraint(equalTo: stackView.widthAnchor).isActive = isDonateEnabled() + + view.layoutIfNeeded() + updateCollection() + } + + func updateCollection() { + socialMediaCollectionView.collectionViewLayout.invalidateLayout() + // On devices with the iOS 12 the actual collectionView layout update not always occurs during the current layout update cycle. + // So constraints update should be performed on the next layout update cycle. + DispatchQueue.main.async { + self.socialMediaCollectionViewHeighConstraint.constant = self.socialMediaCollectionView.collectionViewLayout.collectionViewContentSize.height + } + } + + func isDonateEnabled() -> Bool { + return Settings.donateUrl() != nil + } + + func buildInfoTableViewData() -> [AboutInfoTableViewCellModel] { + let infoContent: [AboutInfo] = [.faq, .reportMapDataProblem, .reportABug, .news, .volunteer, .rateTheApp] + let data = infoContent.map { [weak self] aboutInfo in + return AboutInfoTableViewCellModel(title: aboutInfo.title, image: aboutInfo.image, didTapHandler: { + switch aboutInfo { + case .faq: + self?.navigationController?.pushViewController(FaqController(), animated: true) + case .reportABug: + guard let link = aboutInfo.link else { fatalError("The recipient link should be provided to report a bug.") } + self?.sendEmailWith(header: "Organic Maps Bugreport", toRecipients: [link]) + case .reportMapDataProblem, .volunteer, .news: + self?.openUrl(aboutInfo.link) + case .rateTheApp: + UIApplication.shared.rateApp() + default: + break + } + }) + } + return data + } + + func buildSocialMediaCollectionViewData() -> [SocialMediaCollectionViewCellModel] { + let socialMediaContent: [SocialMedia] = [.telegram, .github, .instagram, .twitter, .linkedin, .organicMapsEmail, .reddit, .matrix, .facebook, .fosstodon] + let data = socialMediaContent.map { [weak self] socialMedia in + return SocialMediaCollectionViewCellModel(image: socialMedia.image, didTapHandler: { + switch socialMedia { + case .telegram: fallthrough + case .github: fallthrough + case .reddit: fallthrough + case .matrix: fallthrough + case .fosstodon: fallthrough + case .facebook: fallthrough + case .twitter: fallthrough + case .instagram: fallthrough + case .linkedin: + self?.openUrl(socialMedia.link, inSafari: true) + case .organicMapsEmail: + guard let link = socialMedia.link else { fatalError("The Organic Maps email link should be provided.") } + self?.sendEmailWith(header: "Organic Maps", toRecipients: [link]) + } + }) + } + return data + } + + // Returns a human-readable maps data version. + static func formattedMapsDataVersion() -> String { + // First, convert version code like 220131 to a date. + let df = DateFormatter() + df.locale = Locale(identifier:"en_US_POSIX") + df.dateFormat = "yyMMdd" + let mapsVersionInt = FrameworkHelper.dataVersion() + let mapsDate = df.date(from: String(mapsVersionInt))! + // Second, print the date in the local user's format. + df.locale = Locale.current + df.dateStyle = .long + df.timeStyle = .none + return df.string(from:mapsDate) + } + + static func formattedAppVersion() -> String { + let appInfo = AppInfo.shared(); + // Use strong left-to-right unicode direction characters for the app version. + return String(format: L("version"), "\u{2066}\(appInfo.bundleVersion)-\(appInfo.buildNumber)\u{2069}") + } + + func showCopyright() { + let path = Bundle.main.path(forResource: "copyright", ofType: "html")! + let html = try! String(contentsOfFile: path, encoding: String.Encoding.utf8) + let webViewController = WebViewController.init(html: html, baseUrl: nil, title: L("copyright"))! + webViewController.openInSafari = true + self.navigationController?.pushViewController(webViewController, animated: true) + } + + func copyToClipboard(_ content: String) { + UIPasteboard.general.string = content + let message = String(format: L("copied_to_clipboard"), content) + UIImpactFeedbackGenerator(style: .medium).impactOccurred() + Toast.toast(withText: message).show(withAlignment: .bottom, pinToSafeArea: false) + } +} + +// MARK: - Actions +private extension AboutController { + @objc func appVersionButtonTapped() { + copyToClipboard(Self.formattedAppVersion()) + } + + @objc func osmMapsDataButtonTapped() { + copyToClipboard(Self.formattedMapsDataVersion()) + } +} + +// MARK: - UITableViewDelegate +extension AboutController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + infoTableViewData[indexPath.row].didTapHandler?() + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return Constants.infoTableViewCellHeight + } +} + +// MARK: - UITableViewDataSource +extension AboutController: UITableViewDataSource { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return infoTableViewData.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(cell: InfoTableViewCell.self, indexPath: indexPath) + let aboutInfo = infoTableViewData[indexPath.row] + cell.set(image: aboutInfo.image, title: aboutInfo.title) + return cell + } +} + +// MARK: - UICollectionViewDataSource +extension AboutController: UICollectionViewDataSource { + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + return socialMediaCollectionViewData.count + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let cell = collectionView.dequeueReusableCell(cell: SocialMediaCollectionViewCell.self, indexPath: indexPath) + cell.setImage(socialMediaCollectionViewData[indexPath.row].image) + return cell + } +} + +// MARK: - UICollectionViewDelegate +extension AboutController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = socialMediaCollectionViewData[indexPath.row] + model.didTapHandler?() + } +} + +// MARK: - UICollectionViewDelegateFlowLayout +extension AboutController: UICollectionViewDelegateFlowLayout { + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + let spacing = Constants.socialMediaCollectionViewSpacing + let numberOfItemsInRowCompact = Constants.socialMediaCollectionNumberOfItemsInRowCompact + let numberOfItemsInRowRegular = Constants.socialMediaCollectionNumberOfItemsInRowRegular + var totalSpacing = (Constants.socialMediaCollectionNumberOfItemsInRowCompact - 1) * spacing + var width = (collectionView.bounds.width - totalSpacing) / numberOfItemsInRowCompact + if traitCollection.verticalSizeClass == .compact || traitCollection.horizontalSizeClass == .regular { + totalSpacing = (numberOfItemsInRowRegular - 1) * spacing + width = (collectionView.bounds.width - totalSpacing) / numberOfItemsInRowRegular + } + let maxWidth = Constants.socialMediaCollectionViewCellMaxWidth + width = min(width, maxWidth) + return CGSize(width: width, height: width) + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + return Constants.socialMediaCollectionViewSpacing + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + return Constants.socialMediaCollectionViewSpacing + } +} + +// MARK: - Mail Composing +private extension AboutController { + func sendEmailWith(header: String, toRecipients: [String]) { + func emailSubject(subject: String) -> String { + let appInfo = AppInfo.shared() + return String(format:"[%@-%@ iOS] %@", appInfo.bundleVersion, appInfo.buildNumber, subject) + } + + func emailBody() -> String { + let appInfo = AppInfo.shared() + return String(format: "\n\n\n\n- %@ (%@)\n- Organic Maps %@-%@\n- %@-%@\n- %@\n", + appInfo.deviceModel, UIDevice.current.systemVersion, + appInfo.bundleVersion, appInfo.buildNumber, + Locale.current.languageCode ?? "", + Locale.current.regionCode ?? "", + Locale.preferredLanguages.joined(separator: ", ")) + } + + func openOutlook(subject: String, body: String, recipients: [String]) -> Bool { + var components = URLComponents(string: "ms-outlook://compose")! + components.queryItems = [ + URLQueryItem(name: "to", value: recipients.joined(separator: ";")), + URLQueryItem(name: "subject", value: subject), + URLQueryItem(name: "body", value: body), + ] + + if let url = components.url, UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + return true + } + return false + } + + func openGmail(subject: String, body: String, recipients: [String]) -> Bool { + var components = URLComponents(string: "googlegmail://co")! + components.queryItems = [ + URLQueryItem(name: "to", value: recipients.joined(separator: ";")), + URLQueryItem(name: "subject", value: subject), + URLQueryItem(name: "body", value: body), + ] + + if let url = components.url, UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + return true + } + return false + } + + let subject = emailSubject(subject: header) + let body = emailBody() + + // Before iOS 14, try to open alternate email apps first, assuming that if users installed them, they're using them. + let os = ProcessInfo().operatingSystemVersion + if (os.majorVersion < 14 && (openGmail(subject: subject, body: body, recipients: toRecipients) || + openOutlook(subject: subject, body: body, recipients: toRecipients))) { + return + } + // From iOS 14, it is possible to change the default mail app, and mailto should open a default mail app. + if MWMMailViewController.canSendMail() { + let vc = MWMMailViewController() + vc.mailComposeDelegate = self + vc.setSubject(subject) + vc.setMessageBody(body, isHTML:false) + vc.setToRecipients(toRecipients) + vc.navigationBar.tintColor = UIColor.whitePrimaryText() + self.present(vc, animated: true, completion:nil) + } else { + let text = String(format:L("email_error_body"), toRecipients.joined(separator: ";")) + let alert = UIAlertController(title: L("email_error_title"), message: text, preferredStyle: .alert) + let action = UIAlertAction(title: L("ok"), style: .default, handler: nil) + alert.addAction(action) + present(alert, animated: true, completion: nil) + } + } +} + +// MARK: - MFMailComposeViewControllerDelegate +extension AboutController: MFMailComposeViewControllerDelegate { + func mailComposeController(_ controller: MFMailComposeViewController, didFinishWith result: MFMailComposeResult, error: Error?) { + self.dismiss(animated: true, completion: nil) + } +} + +// MARK: - UIStackView + AddArrangedSubviewWithSeparator +private extension UIStackView { + func addArrangedSubviewWithSeparator(_ view: UIView) { + if !arrangedSubviews.isEmpty { + let separator = UIView() + separator.setStyleAndApply("Divider") + separator.isUserInteractionEnabled = false + separator.translatesAutoresizingMaskIntoConstraints = false + addArrangedSubview(separator) + NSLayoutConstraint.activate([ + separator.heightAnchor.constraint(equalToConstant: 1.0), + separator.leadingAnchor.constraint(equalTo: leadingAnchor), + separator.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + } + addArrangedSubview(view) + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Models/AboutInfo.swift b/iphone/Maps/UI/Help/AboutController/Models/AboutInfo.swift new file mode 100644 index 0000000000..85a415cfe7 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Models/AboutInfo.swift @@ -0,0 +1,71 @@ +enum AboutInfo { + case faq + case reportABug + case reportMapDataProblem + case volunteer + case news + case rateTheApp + case noTracking + case noWifi + case community + + var title: String { + switch self { + + case .faq: + return L("faq") + case .reportABug: + return L("report_a_bug") + case .reportMapDataProblem: + return L("report_incorrect_map_bug") + case .volunteer: + return L("volunteer") + case .news: + return L("news") + case .rateTheApp: + return L("rate_the_app") + case .noTracking: + return L("about_proposition_1") + case .noWifi: + return L("about_proposition_2") + case .community: + return L("about_proposition_3") + } + } + + var image: UIImage? { + switch self { + case .faq: + return UIImage(named: "ic_about_faq")! + case .reportABug: + return UIImage(named: "ic_about_report_bug")! + case .reportMapDataProblem: + return UIImage(named: "ic_about_report_osm")! + case .volunteer: + return UIImage(named: "ic_about_volunteer")! + case .news: + return UIImage(named: "ic_about_news")! + case .rateTheApp: + return UIImage(named: "ic_about_rate_app")! + case .noTracking, .noWifi, .community: + // Dots are used for these cases + return nil + } + } + + var link: String? { + switch self { + case .faq, .rateTheApp, .noTracking, .noWifi, .community: + // These cases don't provide redirection to the web + return nil + case .reportABug: + return "ios@organicmaps.app" + case .reportMapDataProblem: + return "https://www.openstreetmap.org/fixthemap" + case .volunteer: + return L("translated_om_site_url") + "support-us/" + case .news: + return L("translated_om_site_url") + "news/" + } + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Models/SocialMedia.swift b/iphone/Maps/UI/Help/AboutController/Models/SocialMedia.swift new file mode 100644 index 0000000000..f23b13bf63 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Models/SocialMedia.swift @@ -0,0 +1,62 @@ +enum SocialMedia { + case telegram + case twitter + case instagram + case facebook + case reddit + case matrix + case fosstodon + case linkedin + case organicMapsEmail + case github + + var link: String? { + switch self { + case .telegram: + return L("telegram_url") + case .github: + return "https://github.com/organicmaps/organicmaps/" + case .linkedin: + return "https://www.linkedin.com/company/organic-maps/" + case .organicMapsEmail: + return "ios@organicmaps.app" + case .matrix: + return "https://matrix.to/#/#organicmaps:matrix.org" + case .fosstodon: + return "https://fosstodon.org/@organicmaps" + case .facebook: + return "https://facebook.com/OrganicMaps" + case .twitter: + return "https://twitter.com/OrganicMapsApp" + case .instagram: + return L("instagram_url") + case .reddit: + return "https://www.reddit.com/r/organicmaps/" + } + } + + var image: UIImage { + switch self { + case .telegram: + return UIImage(named: "ic_social_media_telegram")! + case .github: + return UIImage(named: "ic_social_media_github")! + case .linkedin: + return UIImage(named: "ic_social_media_linkedin")! + case .organicMapsEmail: + return UIImage(named: "ic_social_media_mail")! + case .matrix: + return UIImage(named: "ic_social_media_matrix")! + case .fosstodon: + return UIImage(named: "ic_social_media_fosstodon")! + case .facebook: + return UIImage(named: "ic_social_media_facebook")! + case .twitter: + return UIImage(named: "ic_social_media_x")! + case .instagram: + return UIImage(named: "ic_social_media_instagram")! + case .reddit: + return UIImage(named: "ic_social_media_reddit")! + } + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Views/ButtonsStackView.swift b/iphone/Maps/UI/Help/AboutController/Views/ButtonsStackView.swift new file mode 100644 index 0000000000..e515734434 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Views/ButtonsStackView.swift @@ -0,0 +1,56 @@ +final class ButtonsStackView: UIView { + + private let stackView = UIStackView() + private var didTapHandlers = [UIButton: (() -> Void)?]() + + override init(frame: CGRect) { + super.init(frame: frame) + setupViews() + arrangeViews() + layoutViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + arrangeViews() + layoutViews() + } + + private func setupViews() { + stackView.distribution = .fillEqually + stackView.axis = .vertical + stackView.spacing = 20 + } + + private func arrangeViews() { + addSubview(stackView) + } + + private func layoutViews() { + stackView.translatesAutoresizingMaskIntoConstraints = false + let offset = CGFloat(20) + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: offset), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -offset), + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } + + @objc private func buttonTapped(_ sender: UIButton) { + guard let didTapHandler = didTapHandlers[sender] else { return } + didTapHandler?() + } + + // MARK: - Public + func addButton(title: String, font: UIFont = .regular14(), didTapHandler: @escaping () -> Void) { + let button = UIButton() + button.setStyleAndApply("FlatPrimaryTransButton") + button.setTitle(title, for: .normal) + button.titleLabel?.font = font + button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) + stackView.addArrangedSubview(button) + didTapHandlers[button] = didTapHandler + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Views/DonationView.swift b/iphone/Maps/UI/Help/AboutController/Views/DonationView.swift new file mode 100644 index 0000000000..d9a85b5a26 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Views/DonationView.swift @@ -0,0 +1,67 @@ +final class DonationView: UIView { + + private let donateTextLabel = UILabel() + private let donateButton = UIButton() + + var donateButtonDidTapHandler: (() -> Void)? + + init() { + super.init(frame: .zero) + setupViews() + arrangeViews() + layoutViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + arrangeViews() + layoutViews() + } + + private func setupViews() { + donateTextLabel.styleName = "regular14:blackPrimaryText" + donateTextLabel.text = L("donate_description") + donateTextLabel.textAlignment = .center + donateTextLabel.lineBreakMode = .byWordWrapping + donateTextLabel.numberOfLines = 0 + + donateButton.styleName = "FlatNormalButton" + donateButton.setTitle(L("donate").localizedUppercase, for: .normal) + donateButton.addTarget(self, action: #selector(donateButtonDidTap), for: .touchUpInside) + } + + private func arrangeViews() { + addSubview(donateTextLabel) + addSubview(donateButton) + } + + private func layoutViews() { + donateTextLabel.translatesAutoresizingMaskIntoConstraints = false + donateButton.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + donateTextLabel.leadingAnchor.constraint(equalTo: leadingAnchor), + donateTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + donateTextLabel.topAnchor.constraint(equalTo: topAnchor), + + donateButton.topAnchor.constraint(equalTo: donateTextLabel.bottomAnchor, constant: 10), + donateButton.widthAnchor.constraint(equalTo: widthAnchor, constant: -40).withPriority(.defaultHigh), + donateButton.widthAnchor.constraint(lessThanOrEqualToConstant: 400).withPriority(.defaultHigh), + donateButton.centerXAnchor.constraint(equalTo: centerXAnchor), + donateButton.heightAnchor.constraint(equalToConstant: 40), + donateButton.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + @objc private func donateButtonDidTap() { + donateButtonDidTapHandler?() + } +} + +private extension NSLayoutConstraint { + func withPriority(_ priority: UILayoutPriority) -> NSLayoutConstraint { + self.priority = priority + return self + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Views/InfoTableViewCell.swift b/iphone/Maps/UI/Help/AboutController/Views/InfoTableViewCell.swift new file mode 100644 index 0000000000..bdc6e5ed9a --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Views/InfoTableViewCell.swift @@ -0,0 +1,33 @@ +final class InfoTableViewCell: UITableViewCell { + + private let infoView = InfoView() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: .default, reuseIdentifier: reuseIdentifier) + setupView() + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + private func setupView() { + backgroundView = UIView() // Set background color to clear + setStyleAndApply("ClearBackground") + contentView.addSubview(infoView) + infoView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + infoView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + infoView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + infoView.topAnchor.constraint(equalTo: contentView.topAnchor), + infoView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + } + + // MARK: - Public + func set(image: UIImage?, title: String) { + infoView.set(image: image, title: title) + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Views/InfoView.swift b/iphone/Maps/UI/Help/AboutController/Views/InfoView.swift new file mode 100644 index 0000000000..14b6b780e2 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Views/InfoView.swift @@ -0,0 +1,81 @@ +final class InfoView: UIView { + + private let stackView = UIStackView() + private let imageView = UIImageView() + private let titleLabel = UILabel() + private lazy var imageViewWidthConstrain = imageView.widthAnchor.constraint(equalToConstant: 0) + + init() { + super.init(frame: .zero) + self.setupView() + self.arrangeViews() + self.layoutViews() + } + + convenience init(image: UIImage?, title: String) { + self.init() + self.set(image: image, title: title) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if #available(iOS 13.0, *), traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection) { + imageView.applyTheme() + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + stackView.axis = .horizontal + stackView.distribution = .fill + stackView.alignment = .center + stackView.spacing = 16 + + titleLabel.setStyleAndApply("regular16:blackPrimaryText") + titleLabel.lineBreakMode = .byWordWrapping + titleLabel.numberOfLines = .zero + + imageView.setStyleAndApply("MWMBlack") + imageView.contentMode = .scaleAspectFit + } + + private func arrangeViews() { + addSubview(stackView) + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(titleLabel) + } + + private func layoutViews() { + stackView.translatesAutoresizingMaskIntoConstraints = false + imageView.translatesAutoresizingMaskIntoConstraints = false + titleLabel.translatesAutoresizingMaskIntoConstraints = false + imageView.setContentHuggingPriority(.defaultHigh, for: .vertical) + imageView.setContentHuggingPriority(.defaultHigh, for: .horizontal) + titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + NSLayoutConstraint.activate([ + stackView.leadingAnchor.constraint(equalTo: leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.topAnchor.constraint(equalTo: topAnchor), + stackView.bottomAnchor.constraint(equalTo: bottomAnchor), + imageView.heightAnchor.constraint(equalToConstant: 24), + imageViewWidthConstrain + ]) + updateImageWidth() + } + + private func updateImageWidth() { + imageViewWidthConstrain.constant = imageView.image == nil ? 0 : 24 + imageView.isHidden = imageView.image == nil + } + + // MARK: - Public + func set(image: UIImage?, title: String) { + imageView.image = image + titleLabel.text = title + updateImageWidth() + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Views/OSMView.swift b/iphone/Maps/UI/Help/AboutController/Views/OSMView.swift new file mode 100644 index 0000000000..35d0ba31e2 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Views/OSMView.swift @@ -0,0 +1,87 @@ +final class OSMView: UIView { + + private let OSMImageView = UIImageView() + private let OSMTextLabel = UILabel() + private var mapDate: String? + + var didTapHandler: (() -> Void)? + + init() { + super.init(frame: .zero) + setupViews() + arrangeViews() + layoutViews() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupViews() + arrangeViews() + layoutViews() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + guard let mapDate, traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return } + OSMTextLabel.attributedText = attributedString(for: mapDate) + } + + // MARK: - Public + func setMapDate(_ mapDate: String) { + self.mapDate = mapDate + OSMTextLabel.attributedText = attributedString(for: mapDate) + } + + // MARK: - Private + private func setupViews() { + OSMImageView.image = UIImage(named: "osm_logo") + + OSMTextLabel.styleName = "regular14:blackPrimaryText" + OSMTextLabel.lineBreakMode = .byWordWrapping + OSMTextLabel.numberOfLines = 0 + OSMTextLabel.isUserInteractionEnabled = true + + let osmDidTapGesture = UITapGestureRecognizer(target: self, action: #selector(osmDidTap)) + OSMTextLabel.addGestureRecognizer(osmDidTapGesture) + } + + private func arrangeViews() { + addSubview(OSMImageView) + addSubview(OSMTextLabel) + } + + private func layoutViews() { + OSMImageView.translatesAutoresizingMaskIntoConstraints = false + OSMTextLabel.translatesAutoresizingMaskIntoConstraints = false + + NSLayoutConstraint.activate([ + OSMImageView.leadingAnchor.constraint(equalTo: leadingAnchor), + OSMImageView.heightAnchor.constraint(equalToConstant: 40), + OSMImageView.widthAnchor.constraint(equalTo: OSMImageView.heightAnchor), + OSMImageView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor), + OSMImageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), + + OSMTextLabel.leadingAnchor.constraint(equalTo: OSMImageView.trailingAnchor, constant: 8), + OSMTextLabel.trailingAnchor.constraint(equalTo: trailingAnchor), + OSMTextLabel.topAnchor.constraint(greaterThanOrEqualTo: topAnchor), + OSMTextLabel.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor), + OSMTextLabel.centerYAnchor.constraint(equalTo: OSMImageView.centerYAnchor) + ]) + } + + @objc private func osmDidTap() { + didTapHandler?() + } + + private func attributedString(for date: String) -> NSAttributedString { + let osmLink = "OpenStreetMap.org" + let attributedString = NSMutableAttributedString(string: String(format: L("osm_presentation"), date.trimmingCharacters(in: .punctuationCharacters)), + attributes: [.font: UIFont.regular14(), + .foregroundColor: StyleManager.shared.theme?.colors.blackPrimaryText] + ) + let linkRange = attributedString.mutableString.range(of: osmLink) + attributedString.addAttribute(.link, value: "https://www.openstreetmap.org/", range: linkRange) + + return attributedString + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Views/SocialMediaCollectionViewCell.swift b/iphone/Maps/UI/Help/AboutController/Views/SocialMediaCollectionViewCell.swift new file mode 100644 index 0000000000..ac7493d474 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Views/SocialMediaCollectionViewCell.swift @@ -0,0 +1,45 @@ +final class SocialMediaCollectionViewCell: UICollectionViewCell { + + private let imageView = UIImageView() + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupView() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + guard traitCollection.userInterfaceStyle != previousTraitCollection?.userInterfaceStyle else { return } + updateImageColor() + } + + private func setupView() { + setStyleAndApply("ClearBackground") + + imageView.contentMode = .scaleAspectFit + imageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(imageView) + + NSLayoutConstraint.activate([ + imageView.topAnchor.constraint(equalTo: contentView.topAnchor), + imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } + + private func updateImageColor() { + imageView.tintColor = StyleManager.shared.theme?.colors.blackPrimaryText + } + + // MARK: - Public + func setImage(_ image: UIImage) { + imageView.image = image + updateImageColor() + } +} diff --git a/iphone/Maps/UI/Help/AboutController/Views/SocialMediaCollectionViewHeader.swift b/iphone/Maps/UI/Help/AboutController/Views/SocialMediaCollectionViewHeader.swift new file mode 100644 index 0000000000..2839f68230 --- /dev/null +++ b/iphone/Maps/UI/Help/AboutController/Views/SocialMediaCollectionViewHeader.swift @@ -0,0 +1,30 @@ +final class SocialMediaCollectionViewHeader: UICollectionReusableView { + + static let reuseIdentifier = String(describing: SocialMediaCollectionViewHeader.self) + + private let titleLabel = UILabel() + + override init(frame: CGRect) { + super.init(frame: frame) + setupView() + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupView() { + addSubview(titleLabel) + titleLabel.setStyleAndApply("regular16:blackPrimaryText") + titleLabel.numberOfLines = 1 + titleLabel.allowsDefaultTighteningForTruncation = true + titleLabel.adjustsFontSizeToFitWidth = true + titleLabel.minimumScaleFactor = 0.5 + } + + // MARK: - Public + func setTitle(_ title: String) { + titleLabel.text = title + } +} diff --git a/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift b/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift index afa7f87b3f..c3aea45aaa 100644 --- a/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift +++ b/iphone/Maps/UI/PlacePage/Components/PlacePageInfoViewController.swift @@ -336,7 +336,7 @@ class PlacePageInfoViewController: UIViewController { private func addToStack(_ viewController: UIViewController) { addChild(viewController) - stackView.addArrangedSubviewWithSeparator(viewController.view) + stackView.addArrangedSubviewWithSeparator(viewController.view, insets: UIEdgeInsets(top: 0, left: 56, bottom: 0, right: 0)) viewController.didMove(toParent: self) } @@ -352,11 +352,11 @@ class PlacePageInfoViewController: UIViewController { } private extension UIStackView { - func addArrangedSubviewWithSeparator(_ view: UIView) { + func addArrangedSubviewWithSeparator(_ view: UIView, insets: UIEdgeInsets = .zero) { if !arrangedSubviews.isEmpty { view.addSeparator(thickness: CGFloat(1.0), color: StyleManager.shared.theme?.colors.blackDividers, - insets: UIEdgeInsets(top: 0, left: 56, bottom: 0, right: 0)) + insets: insets) } addArrangedSubview(view) }