diff --git a/.github/workflows/CI_iOS.yml b/.github/workflows/CI_iOS.yml index c9cbe2d..94eaa19 100644 --- a/.github/workflows/CI_iOS.yml +++ b/.github/workflows/CI_iOS.yml @@ -11,16 +11,16 @@ jobs: runs-on: macos-13 - timeout-minutes: 10 + timeout-minutes: 15 steps: - uses: actions/checkout@v3 - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode_14.3.app + run: sudo xcode-select -switch /Applications/Xcode_15.0.app - name: Xcode version run: /usr/bin/xcodebuild -version - name: Build and test - run: xcodebuild clean build test -project WeatherApp.xcodeproj -scheme "CI_iOS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 14,OS=16.4" ONLY_ACTIVE_ARCH=YES + run: xcodebuild clean build test -project WeatherApp.xcodeproj -scheme "CI_iOS" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 15 Pro,OS=17.0" ONLY_ACTIVE_ARCH=YES diff --git a/.swiftlint.yml b/.swiftlint.yml deleted file mode 100644 index e7787fd..0000000 --- a/.swiftlint.yml +++ /dev/null @@ -1,10 +0,0 @@ -disabled_rules: # rule identifiers turned on by default to exclude from running - - trailing_whitespace - - closure_parameter_position - - unused_closure_parameter - -excluded: # case-sensitive paths to ignore during linting. Takes precedence over `included` - - WeatherAppTests - - WeatherAppEndToEndTests - - WeatherAppCacheIntegrationTests - - WeatherApp-iOSTests diff --git a/WeatherApp-iOS/Views/PageView/PageControl.swift b/WeatherApp-iOS/Views/PageView/PageControl.swift new file mode 100644 index 0000000..3c20a3c --- /dev/null +++ b/WeatherApp-iOS/Views/PageView/PageControl.swift @@ -0,0 +1,45 @@ +// +// PageControl.swift +// WeatherApp-iOS +// +// Created by Alex Motoc on 01.09.2023. +// + +import SwiftUI +import UIKit + +struct PageControl: UIViewRepresentable { + let numberOfPages: Int + @Binding var currentPage: Int + + func makeCoordinator() -> Coordinator { + Coordinator(control: self) + } + + func makeUIView(context: Context) -> UIPageControl { + let control = UIPageControl() + control.numberOfPages = numberOfPages + control.addTarget( + context.coordinator, + action: #selector(Coordinator.updateCurrentPage(sender:)), + for: .valueChanged + ) + return control + } + + func updateUIView(_ uiView: UIPageControl, context: Context) { + uiView.currentPage = currentPage + } + + class Coordinator: NSObject { + let control: PageControl + + init(control: PageControl) { + self.control = control + } + + @objc func updateCurrentPage(sender: UIPageControl) { + control.currentPage = sender.currentPage + } + } +} diff --git a/WeatherApp-iOS/Views/PageView/PageView.swift b/WeatherApp-iOS/Views/PageView/PageView.swift new file mode 100644 index 0000000..4c8c293 --- /dev/null +++ b/WeatherApp-iOS/Views/PageView/PageView.swift @@ -0,0 +1,21 @@ +// +// PageView.swift +// WeatherApp-iOS +// +// Created by Alex Motoc on 01.09.2023. +// + +import SwiftUI + +struct PageView: View { + let pages: [Page] + @State private var currentPage: Int = 0 + + var body: some View { + ZStack(alignment: .bottom) { + PageViewController(pages: pages, currentPage: $currentPage) + PageControl(numberOfPages: pages.count, currentPage: $currentPage) + .padding(.horizontal) + } + } +} diff --git a/WeatherApp-iOS/Views/PageView/PageViewController.swift b/WeatherApp-iOS/Views/PageView/PageViewController.swift new file mode 100644 index 0000000..318431d --- /dev/null +++ b/WeatherApp-iOS/Views/PageView/PageViewController.swift @@ -0,0 +1,79 @@ +// +// PageViewController.swift +// WeatherApp-iOS +// +// Created by Alex Motoc on 01.09.2023. +// + +import SwiftUI +import UIKit + +struct PageViewController: UIViewControllerRepresentable { + + let pages: [Page] + + @Binding var currentPage: Int + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: Context) -> UIPageViewController { + let pageVC = UIPageViewController( + transitionStyle: .scroll, + navigationOrientation: .horizontal + ) + pageVC.dataSource = context.coordinator + pageVC.delegate = context.coordinator + return pageVC + } + + func updateUIViewController(_ uiViewController: UIPageViewController, context: Context) { + uiViewController.setViewControllers( + [context.coordinator.controllers[currentPage]], + direction: .forward, + animated: true + ) + } + + class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate { + let parent: PageViewController + let controllers: [UIViewController] + + init(_ parent: PageViewController) { + self.parent = parent + self.controllers = parent.pages.map { UIHostingController(rootView: $0) } + } + + func pageViewController( + _ pageViewController: UIPageViewController, + viewControllerBefore viewController: UIViewController + ) -> UIViewController? { + guard let index = controllers.firstIndex(of: viewController) else { return nil } + if index == 0 { return nil } + return controllers[index - 1] + } + + func pageViewController( + _ pageViewController: UIPageViewController, + viewControllerAfter viewController: UIViewController + ) -> UIViewController? { + guard let index = controllers.firstIndex(of: viewController) else { return nil } + if index + 1 == controllers.count { return nil } + return controllers[index + 1] + } + + func pageViewController( + _ pageViewController: UIPageViewController, + didFinishAnimating finished: Bool, + previousViewControllers: [UIViewController], + transitionCompleted completed: Bool + ) { + if completed, + let visibleViewController = pageViewController.viewControllers?.first, + let index = controllers.firstIndex(of: visibleViewController) { + parent.currentPage = index + } + } + } +} diff --git a/WeatherApp-iOS/Views/WeatherTab.swift b/WeatherApp-iOS/Views/WeatherTab.swift index ebf9871..0357d39 100644 --- a/WeatherApp-iOS/Views/WeatherTab.swift +++ b/WeatherApp-iOS/Views/WeatherTab.swift @@ -15,25 +15,18 @@ struct WeatherTab: View { var body: some View { if viewModel.isLocationPermissionGranted { - weatherContent + PageView(pages: makeWeatherPages(isLocationPermissionGranted: true)) + .id(UUID()) + .edgesIgnoringSafeArea(.top) } else { - noLocationPermissionView + let weatherPages = makeWeatherPages(isLocationPermissionGranted: false).map { AnyView($0) } + let pages: [AnyView] = [AnyView(noLocationPermissionView)] + weatherPages + PageView(pages: pages) + .id(UUID()) + .edgesIgnoringSafeArea(.top) } } - @ViewBuilder - var weatherContent: some View { - let weather = store.weatherInformation.first(where: { $0.isCurrentLocation }) - WeatherView(weatherInfo: .init( - info: weather ?? viewModel.emptyWeather, - temperatureType: appSettings.temperatureType, - lastUpdated: viewModel.lastUpdated, - onRefresh: { - Task { await viewModel.getWeather() } - } - )) - } - @ViewBuilder var noLocationPermissionView: some View { VStack(spacing: 20) { @@ -43,4 +36,21 @@ struct WeatherTab: View { } .padding() } + + func makeWeatherPages(isLocationPermissionGranted: Bool) -> [WeatherView] { + var array = store.weatherInformation + if array.isEmpty && isLocationPermissionGranted { + array = [viewModel.emptyWeather] + } + return array.map { + WeatherView(weatherInfo: .init( + info: $0, + temperatureType: appSettings.temperatureType, + lastUpdated: viewModel.lastUpdated, + onRefresh: { + Task { await viewModel.getWeather() } + }) + ) + } + } } diff --git a/WeatherApp.xcodeproj/project.pbxproj b/WeatherApp.xcodeproj/project.pbxproj index 742a302..609c0bb 100644 --- a/WeatherApp.xcodeproj/project.pbxproj +++ b/WeatherApp.xcodeproj/project.pbxproj @@ -54,6 +54,9 @@ 8756CAC22A9E63DC008F99C0 /* UIViewController+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CAC12A9E63DC008F99C0 /* UIViewController+Utils.swift */; }; 8756CAC52A9F6A5A008F99C0 /* ZoomableImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CAC42A9F6A5A008F99C0 /* ZoomableImageViewController.swift */; }; 8756CAC72A9F6B13008F99C0 /* UIView+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CAC62A9F6B13008F99C0 /* UIView+Utils.swift */; }; + 8756CAC92AA1F992008F99C0 /* PageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CAC82AA1F992008F99C0 /* PageViewController.swift */; }; + 8756CACC2AA20FF1008F99C0 /* PageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CACB2AA20FF1008F99C0 /* PageView.swift */; }; + 8756CACE2AA2111C008F99C0 /* PageControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8756CACD2AA2111C008F99C0 /* PageControl.swift */; }; 87768C222A9652040050B4AC /* WeatherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87768C212A9652040050B4AC /* WeatherView.swift */; }; 87768C252A9654CB0050B4AC /* WeatherInfoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87768C242A9654CB0050B4AC /* WeatherInfoViewModel.swift */; }; 87768C282A965D8E0050B4AC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 87768C2A2A965D8E0050B4AC /* Localizable.strings */; }; @@ -210,6 +213,9 @@ 8756CAC12A9E63DC008F99C0 /* UIViewController+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Utils.swift"; sourceTree = ""; }; 8756CAC42A9F6A5A008F99C0 /* ZoomableImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableImageViewController.swift; sourceTree = ""; }; 8756CAC62A9F6B13008F99C0 /* UIView+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Utils.swift"; sourceTree = ""; }; + 8756CAC82AA1F992008F99C0 /* PageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageViewController.swift; sourceTree = ""; }; + 8756CACB2AA20FF1008F99C0 /* PageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageView.swift; sourceTree = ""; }; + 8756CACD2AA2111C008F99C0 /* PageControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageControl.swift; sourceTree = ""; }; 87768C212A9652040050B4AC /* WeatherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherView.swift; sourceTree = ""; }; 87768C242A9654CB0050B4AC /* WeatherInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherInfoViewModel.swift; sourceTree = ""; }; 87768C292A965D8E0050B4AC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -572,6 +578,16 @@ path = ZoomableImage; sourceTree = ""; }; + 8756CACA2AA20FA4008F99C0 /* PageView */ = { + isa = PBXGroup; + children = ( + 8756CACB2AA20FF1008F99C0 /* PageView.swift */, + 8756CAC82AA1F992008F99C0 /* PageViewController.swift */, + 8756CACD2AA2111C008F99C0 /* PageControl.swift */, + ); + path = PageView; + sourceTree = ""; + }; 87768C232A9654BF0050B4AC /* WeatherView */ = { isa = PBXGroup; children = ( @@ -643,6 +659,7 @@ 87768C2F2A966D670050B4AC /* SettingsTab.swift */, 87768C3D2A976E2E0050B4AC /* MapTab */, 87768C232A9654BF0050B4AC /* WeatherView */, + 8756CACA2AA20FA4008F99C0 /* PageView */, ); path = Views; sourceTree = ""; @@ -789,7 +806,6 @@ 874C64632A94F63C00D0185F /* Frameworks */, 874C64642A94F63C00D0185F /* Resources */, 877D49D72A95034800AA41D3 /* Embed Frameworks */, - 87DC20272A98D5170061A604 /* Swiftlint */, ); buildRules = ( ); @@ -845,7 +861,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1430; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1500; TargetAttributes = { 8704C4E82A90CDCE0076BB69 = { CreatedOnToolsVersion = 14.3.1; @@ -941,28 +957,6 @@ }; /* End PBXResourcesBuildPhase section */ -/* Begin PBXShellScriptBuildPhase section */ - 87DC20272A98D5170061A604 /* Swiftlint */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = Swiftlint; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [[ \"$(uname -m)\" == arm64 ]]; then\n export PATH=\"/opt/homebrew/bin:$PATH\"\nfi\n\nif which swiftlint > /dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; - }; -/* End PBXShellScriptBuildPhase section */ - /* Begin PBXSourcesBuildPhase section */ 8704C4E52A90CDCE0076BB69 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -1052,9 +1046,11 @@ 87768C492A97A0D90050B4AC /* SearchSuggestionsViewController.swift in Sources */, 87768C352A96A4CB0050B4AC /* WeatherViewHelpers.swift in Sources */, 87DC20292A98DDB10061A604 /* CLLocation+Utils.swift in Sources */, + 8756CAC92AA1F992008F99C0 /* PageViewController.swift in Sources */, 877D49E02A9517CC00AA41D3 /* WeatherViewModel.swift in Sources */, 87768C332A96A4AC0050B4AC /* ForecastViewModel.swift in Sources */, 87768C442A9797C30050B4AC /* FavouritesListViewController.swift in Sources */, + 8756CACC2AA20FF1008F99C0 /* PageView.swift in Sources */, 8756CABF2A9E629B008F99C0 /* PlaceCell.swift in Sources */, 877D49E72A953D4800AA41D3 /* WeatherInformation+Mock.swift in Sources */, 87768C4B2A97D0030050B4AC /* FavouritesListViewController+Utils.swift in Sources */, @@ -1070,6 +1066,7 @@ 87768C392A9741CF0050B4AC /* FavouritesTab.swift in Sources */, 877D49DC2A950A4E00AA41D3 /* CLLocationManager+Utils.swift in Sources */, 87ACFBAE2A9552B500068877 /* LocationManager.swift in Sources */, + 8756CACE2AA2111C008F99C0 /* PageControl.swift in Sources */, 87ACFBBE2A9609B600068877 /* WeatherInformationStore.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1179,9 +1176,11 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -1242,9 +1241,11 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -1268,9 +1269,11 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = F48VQGX2CM; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1307,9 +1310,11 @@ isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = F48VQGX2CM; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1347,6 +1352,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = F48VQGX2CM; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -1363,6 +1369,7 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = F48VQGX2CM; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -1378,6 +1385,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = F48VQGX2CM; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -1393,6 +1401,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = F48VQGX2CM; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -1520,6 +1529,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = F48VQGX2CM; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; @@ -1535,6 +1545,7 @@ buildSettings = { CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = F48VQGX2CM; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; diff --git a/WeatherApp.xcodeproj/xcshareddata/xcschemes/CI.xcscheme b/WeatherApp.xcodeproj/xcshareddata/xcschemes/CI.xcscheme index b1e4f55..887597b 100644 --- a/WeatherApp.xcodeproj/xcshareddata/xcschemes/CI.xcscheme +++ b/WeatherApp.xcodeproj/xcshareddata/xcschemes/CI.xcscheme @@ -1,6 +1,6 @@