├── .github ├── CODEOWNERS └── workflows │ ├── danger.yml │ ├── documentation.yml │ └── build.yml ├── Example ├── AltSwiftUIExample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── icon.imageset │ │ │ ├── Group 7.png │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── ExampleViews │ │ ├── TextExampleView.swift │ │ ├── SecureFieldExampleView.swift │ │ ├── TextFieldExampleView.swift │ │ ├── ScrollView2AxisExampleView.swift │ │ ├── MenuExampleView.swift │ │ ├── ListTextFieldExampleView.swift │ │ ├── ScrollViewTextFieldExample.swift │ │ ├── NavigationExampleView.swift │ │ ├── AlertsExampleView.swift │ │ ├── ListExampleView.swift │ │ ├── StackUpdateExample.swift │ │ └── ShapesExampleView.swift │ ├── AppDelegate.swift │ ├── Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── ViewController.swift ├── AltSwiftUIExampleTests │ ├── AltSwiftUIExampleTests.swift │ └── Info.plist ├── AltSwiftUIExample.xcodeproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── AltSwiftUIExampleUITests │ ├── Info.plist │ └── AltSwiftUIExampleUITests.swift ├── AltSwiftUI.xcodeproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Dangerfile.swift ├── .jazzy.yaml ├── Sources └── AltSwiftUI │ ├── Source │ ├── Views │ │ ├── Collections │ │ │ ├── Protocols │ │ │ │ ├── Stack.swift │ │ │ │ ├── CollectionProtocols.swift │ │ │ │ └── LazyStack.swift │ │ │ ├── Group.swift │ │ │ ├── LazyVStack.swift │ │ │ ├── LazyHStack.swift │ │ │ ├── TabView.swift │ │ │ ├── VStack.swift │ │ │ ├── ForEach.swift │ │ │ ├── ZStack.swift │ │ │ └── HStack.swift │ │ ├── ReadOnly │ │ │ ├── GeometryReader.swift │ │ │ ├── Spacer.swift │ │ │ ├── Divider.swift │ │ │ ├── Color.swift │ │ │ └── Image.swift │ │ ├── Controls │ │ │ ├── SecureField.swift │ │ │ ├── Toggle.swift │ │ │ ├── Slider.swift │ │ │ ├── DatePicker.swift │ │ │ ├── Button.swift │ │ │ ├── Menu.swift │ │ │ └── TextField.swift │ │ ├── Shapes │ │ │ ├── Rectangle.swift │ │ │ ├── Ellipse.swift │ │ │ ├── Capsule.swift │ │ │ ├── RoundedRectangle.swift │ │ │ └── Circle.swift │ │ └── CoreViews.swift │ ├── Constants │ │ └── SwiftUIConstants.swift │ ├── Helpers │ │ ├── UIApplicationExtensions.swift │ │ └── FoundationExtensions.swift │ ├── State │ │ ├── ObservableObject.swift │ │ ├── StateTypes.swift │ │ ├── Environment.swift │ │ ├── Binding.swift │ │ ├── Published.swift │ │ ├── ObservedObject.swift │ │ ├── State.swift │ │ ├── StateObject.swift │ │ └── EnvironmentObject.swift │ ├── UIKitViews │ │ ├── UIKitReadOnlyViews.swift │ │ └── UIKitNavigationViews.swift │ ├── ViewProperties │ │ ├── ViewPropertyTypes │ │ │ ├── ViewPropertyNavigationTypes.swift │ │ │ ├── ViewPropertyTransitionTypes.swift │ │ │ ├── ViewPropertyViewTypes.swift │ │ │ └── ViewPropertyGestureTypes.swift │ │ └── ViewBuilder.swift │ ├── Previews │ │ └── AltPreviewProvider.swift │ ├── CoreUI │ │ ├── UIHostingController.swift │ │ ├── ViewBinder.swift │ │ └── LayoutSolver.swift │ └── UIKitCompat │ │ └── UIKitCompat.swift │ ├── AltSwiftUI.h │ └── Info.plist ├── Tests └── AltSwiftUITests │ ├── Info.plist │ ├── OperationQueueTests.swift │ └── ViewBinderTests.swift ├── AltSwiftUI.podspec ├── LICENSE ├── Package.swift ├── .swiftlint-danger.yml ├── .swiftlint.yml ├── .gitignore ├── CONTRIBUTING.md └── docResources ├── Features.md └── altswiftui.svg /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @kevinwl02 -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/Assets.xcassets/icon.imageset/Group 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rakutentech/AltSwiftUI/HEAD/Example/AltSwiftUIExample/Assets.xcassets/icon.imageset/Group 7.png -------------------------------------------------------------------------------- /AltSwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Dangerfile.swift: -------------------------------------------------------------------------------- 1 | import Danger 2 | 3 | // MARK: - Check routine 4 | let danger = Danger() 5 | 6 | // SwiftLint format check. 7 | SwiftLint.lint(.modifiedAndCreatedFiles(directory: nil), inline: true, configFile: ".swiftlint-danger.yml") 8 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExampleTests/AltSwiftUIExampleTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AltSwiftUIExampleTests.swift 3 | // AltSwiftUIExampleTests 4 | // 5 | // Created by Wong, Kevin a on 2019/08/26. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import AltSwiftUI 11 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /AltSwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/workflows/danger.yml: -------------------------------------------------------------------------------- 1 | name: Danger CI 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '.swiftlint-danger.yml' 7 | - '**/*.swift' 8 | jobs: 9 | Danger: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v1 13 | - name: Danger 14 | uses: 417-72KI/danger-swiftlint@v1 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | 18 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | readme: README.md 2 | theme: fullwidth 3 | source_directory: ./ 4 | github_url: https://github.com/rakutentech/AltSwiftUI 5 | hide_documentation_coverage: true 6 | undocumented_text: "" 7 | custom_categories_unlisted_prefix: "" 8 | custom_categories: 9 | - name: State 10 | children: 11 | - Binding 12 | - Environment 13 | - EnvironmentObject 14 | - ObservedObject 15 | - Published 16 | - State 17 | - StateObject 18 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/Assets.xcassets/icon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Group 7.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | DEVELOPER_DIR: /Applications/Xcode_12_beta.app/Contents/Developer 9 | 10 | jobs: 11 | deploy_docs: 12 | 13 | runs-on: macos-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Publish Jazzy Docs 18 | uses: steven0351/publish-jazzy-docs@v1 19 | with: 20 | personal_access_token: ${{ secrets.DOCUMENTATION }} 21 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/Protocols/Stack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Stack.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin | Kevs | TDD on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol Stack: View, Renderable { 11 | var viewContent: [View] { get } 12 | var subviewIsEquallySpaced: (View) -> Bool { get } 13 | var setSubviewEqualDimension: (UIView, UIView) -> Void { get } 14 | func updateView(_ view: UIView, context: Context, oldViewContent: [View]?) 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | DEVELOPER_DIR: /Applications/Xcode_13.0.app/Contents/Developer 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Run tests 20 | run: xcodebuild test -project AltSwiftUI.xcodeproj -scheme AltSwiftUITests -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 8,OS=latest" 21 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/AltSwiftUI.h: -------------------------------------------------------------------------------- 1 | // 2 | // AltSwiftUI.h 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/09/14. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for AltSwiftUI. 11 | FOUNDATION_EXPORT double AltSwiftUIVersionNumber; 12 | 13 | //! Project version string for AltSwiftUI. 14 | FOUNDATION_EXPORT const unsigned char AltSwiftUIVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/TextExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2020/10/13. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | 11 | struct TextExampleView: View { 12 | var viewStore = ViewValues() 13 | var body: View { 14 | VStack { 15 | Text("Properties") 16 | .strikethrough(true, color: .red) 17 | .underline(true, color: .green) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/Protocols/CollectionProtocols.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionProtocols.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/05. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ComparableViewGrouper { 12 | func iterateDiff(oldViewGroup: ComparableViewGrouper, startDisplayIndex: inout Int, iterate: (Int, DiffableViewSourceOperation) -> Void) 13 | var viewContent: [View] { get } 14 | } 15 | 16 | protocol ViewGrouper { 17 | var viewContent: [View] { get } 18 | } 19 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/SecureFieldExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureFieldExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Elvis Lin on 2020/12/24. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | 11 | struct SecureFieldExampleView: View { 12 | var viewStore = ViewValues() 13 | 14 | @State private var field: String = "" 15 | @State private var isFirstResponder = true 16 | 17 | var body: View { 18 | VStack(alignment: .center) { 19 | SecureField("Title", text: $field) 20 | .firstResponder($isFirstResponder) 21 | .background(Color.yellow) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Constants/SwiftUIConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIConstants.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2019/10/07. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum SwiftUIConstants { 12 | static let defaultCellHeight: CGFloat = 44 13 | static let defaultPadding: CGFloat = 10 14 | static let defaultCellPadding: CGFloat = 12 15 | static let defaultSpacing: CGFloat = 5 16 | static var systemGray: UIColor { 17 | if #available(iOS 13.0, *) { 18 | return UIColor.systemGray4 19 | } else { 20 | return UIColor(white: 0.9, alpha: 1) 21 | } 22 | } 23 | static let minHeaderHeight: CGFloat = 35 24 | } 25 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Helpers/UIApplicationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Tanabe, Alex on 2020/07/20. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIApplication { 12 | var activeWindow: UIWindow? { 13 | if #available(iOS 13, *), 14 | let foregroundActiveScene = UIApplication.shared.connectedScenes 15 | .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene, 16 | let activeWindow = (foregroundActiveScene.delegate as? UIWindowSceneDelegate)?.window { 17 | return activeWindow 18 | } else { 19 | return keyWindow 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Helpers/FoundationExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FoundationExtensions.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/01/22. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Array { 12 | subscript(safe index: Index) -> Element? { 13 | get { 14 | indices.contains(index) ? self[index] : nil 15 | } 16 | set { 17 | if let newValue = newValue, indices.contains(index) { 18 | self[index] = newValue 19 | } 20 | } 21 | } 22 | } 23 | 24 | extension URL { 25 | init?(stringToUrlEncode: String) { 26 | self.init(string: stringToUrlEncode.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/AltSwiftUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExampleTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExampleUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2019/08/26. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AltSwiftUI 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | private var mainController: UIViewController? 17 | 18 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 19 | 20 | window = UIWindow() 21 | mainController = UIHostingController(rootView: ExampleView()) 22 | window?.rootViewController = mainController 23 | window?.makeKeyAndVisible() 24 | 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/ObservableObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservableObject.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An object that can be observed. Use this in conjunction with 12 | /// `Published` property wrappers inside the object and 13 | /// `ObservedObject` property wrappers for observing the object from a view. 14 | public protocol ObservableObject: AnyObject { 15 | } 16 | 17 | extension ObservableObject { 18 | func setupPublishedValues() { 19 | let mirror = Mirror(reflecting: self) 20 | for child in mirror.children { 21 | if let childWrapper = child.value as? ChildWrapper { 22 | childWrapper.setParent(self) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/Group.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Group.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/05. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A group is a convenience view that groups multiple views into a single view. This 12 | /// view has no visual representation and does not affect the subviews layouts in 13 | /// any way. 14 | /// 15 | /// Use this when the number of elements inside a `ViewBuilder` exceeds the limit. 16 | public struct Group: View, ViewGrouper { 17 | public var viewStore = ViewValues() 18 | var viewContent: [View] 19 | public init(@ViewBuilder content: () -> View) { 20 | viewContent = content().subViews 21 | } 22 | public var body: View { 23 | viewContent.first ?? EmptyView() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /AltSwiftUI.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "AltSwiftUI" 3 | s.version = "1.5.0" 4 | s.ios.deployment_target = "11.0" 5 | s.swift_version = "5.3" 6 | s.summary = "Open Source UI framework based on SwiftUI syntax and features, adding backwards compatibility." 7 | s.description = <<-DESC 8 | Available from iOS 11 using Xcode 12. AltSwiftUI has some small differences to SwiftUI, where it 9 | handles certain features slightly differently and adds some missing features as well. 10 | DESC 11 | s.homepage = "https://github.com/rakutentech/AltSwiftUI" 12 | s.license = "MIT" 13 | s.author = { "Kevin Wong" => "kevin.a.wong@rakuten.com" } 14 | s.source = { :git => "https://github.com/rakutentech/AltSwiftUI.git", :tag => "#{s.version}" } 15 | s.source_files = ["Sources/AltSwiftUI/Source/*/*.swift", "Sources/AltSwiftUI/Source/*/*/*.swift", "Sources/AltSwiftUI/Source/*/*/*/*.swift"] 16 | end 17 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/ReadOnly/GeometryReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeometryReader.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/21. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A container view that provides its children a 12 | /// `GeometryProxy` value with it's own frame. 13 | /// 14 | /// By default this view's dimensions are flexible. 15 | public struct GeometryReader: View { 16 | public var viewStore = ViewValues() 17 | var viewContent: (GeometryProxy) -> View 18 | @State private var geometryProxy: GeometryProxy = .default 19 | 20 | public init(@ViewBuilder content: @escaping (GeometryProxy) -> View) { 21 | viewContent = content 22 | } 23 | 24 | public var body: View { 25 | ZStack(alignment: .topLeading) { 26 | viewContent(geometryProxy) 27 | } 28 | .frame(maxWidth: .infinity, maxHeight: .infinity) 29 | .geometryListener($geometryProxy) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/TextFieldExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Lin, YingChieh on 2021/03/09. 6 | // Copyright © 2021 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | 11 | struct TextFieldExampleView: View { 12 | var viewStore = ViewValues() 13 | 14 | @State private var count = 0 15 | @State private var field: String = "" 16 | @State private var isFirstResponder = true 17 | 18 | var body: View { 19 | VStack(alignment: .center) { 20 | TextField("Search Keyword", text: $field, onCommit: { 21 | commitSearchField() 22 | }) 23 | .firstResponder($isFirstResponder) 24 | .background(Color.yellow) 25 | .foregroundColor(Color.purple) 26 | .padding(.all, 16) 27 | 28 | Text("Search Keyword \(count) times") 29 | .underline(true, color: .red) 30 | } 31 | } 32 | 33 | private func commitSearchField() { 34 | count += 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/StateTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateTypes.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class DynamicPropertyHolder { 12 | var value: Value 13 | init(value: Value) { 14 | self.value = value 15 | } 16 | } 17 | 18 | /// Represents a stored variable in a `View` type that is dynamically 19 | /// updated from some external property of the view. These variables 20 | /// will be given valid values immediately before `body()` is called. 21 | protocol DynamicProperty { 22 | 23 | /// Called immediately before the view's body() function is 24 | /// executed, after updating the values of any dynamic properties 25 | /// stored in `self`. 26 | func update(context: Context) 27 | } 28 | 29 | /// An object that can have it's internal value retrieved and changed 30 | /// externally. 31 | protocol MigratableProperty: AnyObject { 32 | var internalValue: Any { get } 33 | func setInternalValue(_ value: Any) 34 | } 35 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/ScrollView2AxisExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollView2AxisExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2021/01/12. 6 | // Copyright © 2021 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | 11 | struct ScrollView2AxisExampleView: View { 12 | var viewStore = ViewValues() 13 | var body: View { 14 | ScrollView(.both) { 15 | VStack { 16 | HStack { 17 | Text("Start") 18 | .lineLimit(1) 19 | Color.green 20 | .frame(width: 100, height: 100) 21 | .padding(.leading, 250) 22 | Text("trailing text") 23 | .lineLimit(1) 24 | .padding(.leading, 250) 25 | } 26 | Color.red 27 | .frame(width: 100, height: 100) 28 | .padding(.top, 500) 29 | Text("bottom text") 30 | .padding(.top, 500) 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Rakuten, Inc. (https://global.rakuten.com/corp/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/MenuExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MenuExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Chan, Chengwei on 2021/03/01. 6 | // Copyright © 2021 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | 11 | struct MenuExampleView: View { 12 | var viewStore = ViewValues() 13 | 14 | @State private var update = false 15 | 16 | var body: View { 17 | VStack { 18 | if #available(iOS 14.0, *) { 19 | Menu { 20 | Button("Option 1", action: { print("click option 1") }) 21 | Button("Option 2", action: {}) 22 | Button("Option 3", action: {}) 23 | 24 | Menu("lv2 Menu") { 25 | ForEach(0..<20) { index in 26 | Button("Option_\(index)", action: {}) 27 | } 28 | } 29 | } label: { () -> View in 30 | VStack { 31 | Text("Menu") 32 | } 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Tests/AltSwiftUITests/OperationQueueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueueTests.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/10/08. 6 | // 7 | 8 | import XCTest 9 | @testable import AltSwiftUI 10 | 11 | class OperationQueueTests: XCTestCase { 12 | func testViewOperationQueue() { 13 | let queue = ViewOperationQueue() 14 | var index = 0 15 | 16 | queue.addOperation { 17 | index += 1 18 | XCTAssert(index == 1) 19 | queue.addOperation { 20 | index += 1 21 | XCTAssert(index == 3) 22 | } 23 | 24 | index += 1 25 | XCTAssert(index == 2) 26 | queue.addOperation { 27 | queue.addOperation { 28 | index += 1 29 | XCTAssert(index == 5) 30 | } 31 | 32 | index += 1 33 | XCTAssert(index == 4) 34 | queue.addOperation { 35 | index += 1 36 | XCTAssert(index == 6) 37 | } 38 | } 39 | } 40 | queue.drainRecursively() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/ReadOnly/Spacer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Spacer.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that expands infinitely as much as it is allowed by 12 | /// its parent view. Spacers should only be used inside `HStack` or 13 | /// `VStack`, otherwise it's behavior will be undefined. 14 | /// 15 | /// When inside a `HStack`, the spacer will expand horizontally. 16 | /// 17 | /// When inside a `VStack`, the spacer will expand vertically. 18 | public struct Spacer: View { 19 | public var viewStore = ViewValues() 20 | 21 | public var body: View { 22 | self 23 | } 24 | 25 | public init() {} 26 | } 27 | 28 | extension Spacer: Renderable { 29 | public func createView(context: Context) -> UIView { 30 | if let direction = context.viewValues?.direction { 31 | return SwiftUIExpandView(direction: direction, ignoreTouch: true) 32 | } else { 33 | return UIView() 34 | } 35 | } 36 | 37 | public func updateView(_ view: UIView, context: Context) { 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "AltSwiftUI", 8 | platforms: [ 9 | .iOS(.v11) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "AltSwiftUI", 15 | targets: ["AltSwiftUI"]) 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "AltSwiftUI", 26 | dependencies: [], 27 | sources: ["Source"]), 28 | .testTarget( 29 | name: "AltSwiftUITests", 30 | dependencies: ["AltSwiftUI"]) 31 | ], 32 | swiftLanguageVersions: [SwiftVersion.v5] 33 | ) 34 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/ListTextFieldExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListTextFieldExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2021/04/15. 6 | // Copyright © 2021 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | import UIKit 11 | 12 | struct ListTextFieldExampleView: View { 13 | var viewStore = ViewValues() 14 | 15 | @State private var text1: String = "" 16 | @State private var text2: String = "" 17 | @State private var keyboardDismissType: UIScrollView.KeyboardDismissMode = .onDrag 18 | 19 | var body: View { 20 | VStack { 21 | Text("Keyboard Dismiss Mode") 22 | HStack { 23 | Button("interactive") { keyboardDismissType = .interactive } 24 | Button("onDrag") { keyboardDismissType = .onDrag } 25 | Button("none") { keyboardDismissType = .none } 26 | } 27 | List { 28 | TextField("Input 1", text: $text1) 29 | Rectangle() 30 | .fill(.red) 31 | .frame(width: 10, height: 1000) 32 | TextField("Input 2", text: $text2) 33 | } 34 | .keyboardDismissMode(keyboardDismissType) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// References and observes environment values set by the framework. 12 | /// 13 | /// Currently supported environment values are: 14 | /// - presentationMode 15 | @propertyWrapper public class Environment: DynamicProperty { 16 | let keyPath: KeyPath 17 | var _wrappedValue: Value? 18 | 19 | public init(_ keyPath: KeyPath) { 20 | self.keyPath = keyPath 21 | } 22 | 23 | /// The internal value of this wrapper type. 24 | public var wrappedValue: Value { 25 | get { 26 | assert(_wrappedValue != nil, "Environment being called outside of body") 27 | // TODO: Register --> EnvironmentHolder.currentBodyViewBinderStack.last?.registerStateNotification(origin: _wrappedValue!) 28 | return _wrappedValue! 29 | } 30 | set { 31 | } 32 | } 33 | 34 | func update(context: Context) { 35 | let values = EnvironmentValues(rootController: context.rootController) 36 | _wrappedValue = values[keyPath: keyPath] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/ScrollViewTextFieldExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScrollViewTextFieldExample.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2021/04/15. 6 | // Copyright © 2021 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | import UIKit 11 | 12 | struct ScrollViewTextFieldExampleView: View { 13 | var viewStore = ViewValues() 14 | 15 | @State private var text1: String = "" 16 | @State private var text2: String = "" 17 | @State private var keyboardDismissType: UIScrollView.KeyboardDismissMode = .onDrag 18 | 19 | var body: View { 20 | VStack { 21 | Text("Keyboard Dismiss Mode") 22 | HStack { 23 | Button("interactive") { keyboardDismissType = .interactive } 24 | Button("onDrag") { keyboardDismissType = .onDrag } 25 | Button("none") { keyboardDismissType = .none } 26 | } 27 | ScrollView { 28 | VStack { 29 | TextField("Input 1", text: $text1) 30 | Rectangle() 31 | .fill(.red) 32 | .frame(width: 10, height: 1000) 33 | TextField("Input 2", text: $text2) 34 | } 35 | } 36 | .keyboardDismissMode(keyboardDismissType) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/UIKitViews/UIKitReadOnlyViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitReadOnlyViews.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SwiftUIImageView: UIImageView, UIKitViewHandler { 12 | deinit { 13 | executeDisappearHandler() 14 | } 15 | override func layoutSubviews() { 16 | super.layoutSubviews() 17 | notifyGeometryListener(frame: frame) 18 | } 19 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 20 | updateOnTraitChange(previousTrait: previousTraitCollection) 21 | } 22 | } 23 | 24 | class SwiftUILabel: UILabel, UIKitViewHandler { 25 | deinit { 26 | executeDisappearHandler() 27 | } 28 | 29 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 30 | super.traitCollectionDidChange(previousTraitCollection) 31 | updateOnTraitChange(previousTrait: previousTraitCollection) 32 | } 33 | override var intrinsicContentSize: CGSize { 34 | let size = super.intrinsicContentSize 35 | let maxSize = sizeThatFits(CGSize(width: CGFloat.greatestFiniteMagnitude, height: 1)) 36 | return CGSize(width: maxSize.width, height: size.height) 37 | } 38 | override func layoutSubviews() { 39 | super.layoutSubviews() 40 | notifyGeometryListener(frame: frame) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/ReadOnly/Divider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Divider.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that creates a line divider for dividing content. 12 | /// 13 | /// When used inside a `VStack`, the divider will display a 14 | /// horizontal line. 15 | /// 16 | /// When used inside a `HStack`, the divider will display a 17 | /// vertical line. 18 | /// 19 | /// Otherwise, the divider will always display a horizontal line. 20 | public struct Divider: View { 21 | public var viewStore = ViewValues() 22 | public var body: View { 23 | self 24 | } 25 | public init() {} 26 | } 27 | 28 | extension Divider: Renderable { 29 | public func createView(context: Context) -> UIView { 30 | let expandDirection: Direction = (context.viewValues?.direction ?? .horizontal) == .horizontal ? .vertical : .horizontal 31 | let view = SwiftUIExpandView(direction: expandDirection, ignoreTouch: true).noAutoresizingMask() 32 | view.backgroundColor = .lightGray 33 | if expandDirection == .horizontal { 34 | view.heightAnchor.constraint(equalToConstant: 1).isActive = true 35 | } else { 36 | view.widthAnchor.constraint(equalToConstant: 1).isActive = true 37 | } 38 | return view 39 | } 40 | 41 | public func updateView(_ view: UIView, context: Context) { 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.swiftlint-danger.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - implicit_return 3 | - modifier_order 4 | - first_where 5 | - fatal_error_message 6 | - empty_string 7 | - empty_count 8 | - convenience_type 9 | - toggle_bool 10 | - redundant_nil_coalescing 11 | - sorted_first_last 12 | - contains_over_first_not_nil 13 | - contains_over_filter_count 14 | - contains_over_filter_is_empty 15 | - last_where 16 | - redundant_type_annotation 17 | - enum_case_associated_values_count 18 | disabled_rules: 19 | - trailing_whitespace 20 | - identifier_name 21 | - file_length 22 | - multiple_closures_with_trailing_closure 23 | - type_name 24 | - unused_setter_value 25 | - type_body_length 26 | - weak_delegate 27 | - function_parameter_count 28 | - cyclomatic_complexity 29 | - nesting 30 | - duplicate_imports 31 | - todo 32 | excluded: 33 | - AltSwiftUITests.swift 34 | function_body_length: 35 | warning: 60 36 | error: 100 37 | line_length: 38 | warning: 400 39 | error: 600 40 | custom_rules: 41 | travel_verb_getter_function: 42 | name: "Verb Prefixed Getter Function" 43 | regex: '(?<=func\s)(get|calculate|fetch|retrieve|generate)\w+\([^\)]*\)[\n\t\s]+\-\>[\n\t\s]+\w+\??[\n\t\s]+\{' 44 | message: "Avoid the use of verbs on getter methods unless its a mutating/nonmutating method pair or a factory method." 45 | travel_danger_dictionary_function: 46 | name: "Dictionary initializer is error prone" 47 | regex: 'Dictionary\(uniqueKeysWithValues' 48 | message: "Avoid use of uniqueKeysWithValues initializer as it is crash prone." 49 | severity: error 50 | 51 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/ViewProperties/ViewPropertyTypes/ViewPropertyNavigationTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewPropertyNavigationTypes.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Public Types 12 | 13 | /// The display mode for the title in a navigation bar. 14 | public enum NavigationBarTitleDisplayMode { 15 | /// Displays large title on the first view, while using 16 | /// inline title for the subsequent views in a navigation stack. 17 | case automatic 18 | 19 | case inline 20 | case large 21 | } 22 | 23 | /// A type that represents the presentation mode of a view. 24 | public struct PresentationMode { 25 | 26 | weak var controller: ScreenViewController? 27 | 28 | /// Indicates whether a view is currently presented. 29 | public internal(set) var isPresented: Bool 30 | 31 | /// Dismisses the view if it is currently presented. 32 | /// 33 | /// If `isPresented` is false, `dismiss()` is a no-op. 34 | public func dismiss() { 35 | controller?.dismissPresentationMode() 36 | } 37 | } 38 | 39 | // MARK: - Internal Types 40 | 41 | struct TabItem { 42 | let text: String 43 | let image: UIImage 44 | } 45 | 46 | struct NavigationButtons { 47 | let leading: [UIBarButtonItem]? 48 | let trailing: [UIBarButtonItem]? 49 | } 50 | 51 | struct SheetPresentation { 52 | let sheetView: View 53 | let onDismiss: () -> Void 54 | let isPresented: Binding 55 | let isFullScreen: Bool 56 | weak var id: UIView? 57 | } 58 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Controls/SecureField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SecureField.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Elvis Lin on 2020/12/1. 6 | // 7 | 8 | import UIKit 9 | 10 | /// A view into which the user securely enters private text. 11 | public struct SecureField: View { 12 | public var viewStore = ViewValues() 13 | 14 | var textField: TextField 15 | 16 | public var body: View { 17 | textField 18 | } 19 | 20 | /// Creates an instance with a a value of type `String`. 21 | /// 22 | /// - Parameters: 23 | /// - title: The title of `self`, used as a placeholder. 24 | /// - text: The text to be displayed and edited. 25 | /// - onCommit: The action to perform when the user performs an action 26 | /// (usually the return key) while the `SecureField` has focus. 27 | public init(_ title: String, text: Binding, onCommit: @escaping () -> Void = {}) { 28 | self.textField = TextField(title, text: text, isSecureTextEntry: true, onCommit: onCommit) 29 | } 30 | 31 | /// Sets if this view is the first responder or not. 32 | /// 33 | /// Setting a value of `true` will make this view become the first 34 | /// responder if not already. 35 | /// Setting a value of `false` will make this view resign beign first 36 | /// responder if it is the first responder. 37 | /// 38 | /// - important: Not SwiftUI compatible. 39 | public func firstResponder(_ firstResponder: Binding) -> Self { 40 | var view = self 41 | view.textField = textField 42 | .firstResponder(firstResponder) 43 | return view 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExampleUITests/AltSwiftUIExampleUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AltSwiftUIExampleUITests.swift 3 | // AltSwiftUIExampleUITests 4 | // 5 | // Created by Wong, Kevin a on 2019/08/26. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | class AltSwiftUIExampleUITests: XCTestCase { 12 | 13 | override func setUp() { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | 16 | // In UI tests it is usually best to stop immediately when a failure occurs. 17 | continueAfterFailure = false 18 | 19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 20 | } 21 | 22 | override func tearDown() { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | } 25 | 26 | func testExample() { 27 | // UI tests must launch the application that they test. 28 | let app = XCUIApplication() 29 | app.launch() 30 | 31 | // Use recording to get started writing UI tests. 32 | // Use XCTAssert and related functions to verify your tests produce the correct results. 33 | } 34 | 35 | func testLaunchPerformance() { 36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 37 | // This measures how long it takes to launch your application. 38 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { 39 | XCUIApplication().launch() 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/NavigationExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigationExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2020/11/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | 11 | struct NavigationExampleView: View { 12 | var viewStore = ViewValues() 13 | 14 | @State private var sheet1 = false 15 | @State private var sheet2 = false 16 | @State private var sheet3 = false 17 | @State private var sheet4 = false 18 | 19 | var body: View { 20 | VStack { 21 | Button("Sheet 1") { 22 | sheet1 = true 23 | } 24 | .sheet(isPresented: $sheet1) { 25 | Text("Sheet 1") 26 | } 27 | 28 | Button("Sheet 2") { 29 | sheet2 = true 30 | } 31 | .sheet(isPresented: $sheet2) { 32 | Text("Sheet 2") 33 | } 34 | 35 | Button("Sheet 3 - NextView") { 36 | sheet3 = true 37 | } 38 | .sheet(isPresented: $sheet3) { 39 | NavigationExampleNextView(show: $sheet3) 40 | } 41 | 42 | Button("Sheet 4 - NextView") { 43 | sheet4 = true 44 | } 45 | .sheet(isPresented: $sheet4) { 46 | NavigationExampleNextView(show: $sheet4) 47 | } 48 | } 49 | } 50 | } 51 | 52 | struct NavigationExampleNextView: View { 53 | var viewStore = ViewValues() 54 | @Binding var show: Bool 55 | 56 | var body: View { 57 | Button("Close") { 58 | show = false 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | opt_in_rules: 2 | - implicit_return 3 | - modifier_order 4 | - first_where 5 | - fatal_error_message 6 | - empty_string 7 | - empty_count 8 | - convenience_type 9 | - toggle_bool 10 | - redundant_nil_coalescing 11 | - sorted_first_last 12 | - contains_over_first_not_nil 13 | - contains_over_filter_count 14 | - contains_over_filter_is_empty 15 | - last_where 16 | - redundant_type_annotation 17 | - enum_case_associated_values_count 18 | disabled_rules: 19 | - trailing_whitespace 20 | - identifier_name 21 | - file_length 22 | - multiple_closures_with_trailing_closure 23 | - type_name 24 | - unused_setter_value 25 | - type_body_length 26 | - weak_delegate 27 | - function_parameter_count 28 | - cyclomatic_complexity 29 | - nesting 30 | - duplicate_imports 31 | excluded: 32 | - AltSwiftUITests.swift 33 | function_body_length: 34 | warning: 60 35 | error: 100 36 | line_length: 37 | warning: 400 38 | error: 600 39 | custom_rules: 40 | travel_verb_getter_function: 41 | name: "Verb Prefixed Getter Function" 42 | regex: '(?<=func\s)(get|calculate|fetch|retrieve|generate)\w+\([^\)]*\)[\n\t\s]+\-\>[\n\t\s]+\w+\??[\n\t\s]+\{' 43 | message: "Avoid the use of verbs on getter methods unless its a mutating/nonmutating method pair or a factory method." 44 | travel_danger_dictionary_function: 45 | name: "Dictionary initializer is error prone" 46 | regex: 'Dictionary\(uniqueKeysWithValues' 47 | message: "Avoid use of uniqueKeysWithValues initializer as it is crash prone." 48 | severity: error 49 | # Uncomment to find closure assignments to non-self variables and possible harder to find retain cycles 50 | # find_non_self_stored_closures: 51 | # name: "Stored closure in variable" 52 | # regex: '[A-z0-1\_\-]+\.[A-z0-1\_\-]+\ \=\ \{' 53 | # message: "Found assignment of stored closure of variable" 54 | 55 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/Binding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Binding.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A binding is a reference to a value. It does not represent a source 12 | /// of truth, but it acts as a proxy being able to read and modify the 13 | /// original value. Reading it's `wrappedValue` from a view's body will 14 | /// trigger a subscription from the view to changes in the originally referenced 15 | /// value. 16 | @propertyWrapper @dynamicMemberLookup 17 | public struct Binding { 18 | private var get: (() -> Value) 19 | private var set: ((Value) -> Void) 20 | 21 | public init(get: @escaping () -> Value, set: @escaping (Value) -> Void) { 22 | self.get = get 23 | self.set = set 24 | } 25 | 26 | /// The value referenced by the binding. Assignments to the value 27 | /// will be immediately visible on reading (assuming the binding 28 | /// represents a mutable location), but the view changes they cause 29 | /// may be processed asynchronously to the assignment. 30 | public var wrappedValue: Value { 31 | get { 32 | get() 33 | } 34 | nonmutating set { 35 | set(newValue) 36 | } 37 | } 38 | 39 | /// The binding value, as "unwrapped" by accessing `$foo` on a `@Binding` property. 40 | public var projectedValue: Binding { 41 | self 42 | } 43 | 44 | /// Creates a new `Binding` focused on `Subject` using a key path. 45 | public subscript(dynamicMember keyPath: WritableKeyPath) -> Binding { 46 | Binding(get: { 47 | self.wrappedValue[keyPath: keyPath] 48 | }, set: { value in 49 | self.wrappedValue[keyPath: keyPath] = value 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Shapes/Rectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Rectangle.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that represents a Rectangle shape. 12 | public struct Rectangle: Shape { 13 | public var viewStore = ViewValues() 14 | 15 | public var fillColor = Color.clear 16 | public var strokeBorderColor = Color.clear 17 | public var style = StrokeStyle() 18 | 19 | public var body: View { 20 | EmptyView() 21 | } 22 | 23 | public init() {} 24 | 25 | public func createView(context: Context) -> UIView { 26 | let view = AltShapeView().noAutoresizingMask() 27 | view.updateOnLayout = { [weak view] rect in 28 | guard let view = view else { return } 29 | updatePath(view: view, path: UIBezierPath(rect: rect), animation: nil) 30 | } 31 | updateView(view, context: context.withoutTransaction) 32 | return view 33 | } 34 | 35 | public func updateView(_ view: UIView, context: Context) { 36 | guard let view = view as? AltShapeView else { return } 37 | let oldView = view.lastRenderableView?.view as? Rectangle 38 | 39 | let width = context.viewValues?.viewDimensions?.width ?? view.bounds.width 40 | let height = context.viewValues?.viewDimensions?.height ?? view.bounds.height 41 | let animation = context.transaction?.animation 42 | view.lastSizeFromViewUpdate = CGSize(width: width, height: height) 43 | 44 | if context.viewValues?.viewDimensions != oldView?.viewStore.viewDimensions { 45 | updatePath(view: view, path: UIBezierPath(rect: CGRect(x: 0, y: 0, width: width, height: height)), animation: animation) 46 | } 47 | updateShapeLayerValues(view: view, context: context) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Controls/Toggle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Toggle.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that can be turned on and off. 12 | public struct Toggle: View { 13 | public var viewStore = ViewValues() 14 | let isOn: Binding 15 | let label: View 16 | 17 | /// Creates an instance of a toggle. 18 | public init(isOn: Binding, @ViewBuilder label: () -> View) { 19 | self.isOn = isOn 20 | self.label = label() 21 | } 22 | public init(_ title: String, isOn: Binding) { 23 | self.isOn = isOn 24 | label = Text(title) 25 | } 26 | public var body: View { 27 | EmptyView() 28 | } 29 | } 30 | 31 | extension Toggle: Renderable { 32 | public func createView(context: Context) -> UIView { 33 | let labelsHidden = context.viewValues?.labelsHidden ?? false 34 | let view = SwiftUISwitch().noAutoresizingMask() 35 | updateView(view, context: context) 36 | 37 | if labelsHidden { 38 | return view 39 | } else { 40 | return SwiftUILabeledView(label: label.renderableView(parentContext: context) ?? UIView(), control: view) 41 | } 42 | } 43 | 44 | public func updateView(_ view: UIView, context: Context) { 45 | if let view = view as? SwiftUISwitch { 46 | setupView(view, context: context) 47 | } else if let view = view as? SwiftUILabeledView { 48 | label.scheduleUpdateRender(uiView: view.label, parentContext: context) 49 | setupView(view.control, context: context) 50 | } 51 | } 52 | 53 | private func setupView(_ view: SwiftUISwitch, context: Context) { 54 | view.isOnBinding = isOn 55 | view.isOn = isOn.wrappedValue 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | # Package.resolved 41 | .build/ 42 | .swiftpm/ 43 | 44 | # CocoaPods 45 | # 46 | # We recommend against adding the Pods directory to your .gitignore. However 47 | # you should judge for yourself, the pros and cons are mentioned at: 48 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 49 | # 50 | Pods/ 51 | 52 | # Carthage 53 | # 54 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 55 | # Carthage/Checkouts 56 | 57 | Carthage/Build 58 | 59 | # fastlane 60 | # 61 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 62 | # screenshots whenever they are needed. 63 | # For more information about the recommended setup visit: 64 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 65 | 66 | fastlane/report.xml 67 | fastlane/Preview.html 68 | fastlane/screenshots 69 | fastlane/test_output 70 | 71 | .DS_Store 72 | 73 | # userstate 74 | 75 | AltSwiftUI/AltSwiftUI.xcodeproj/xcuserdata/ 76 | AltSwiftUI/AltSwiftUI.xcodeproj/project.xcworkspace/xcuserdata/ 77 | Example/AltSwiftUIExample.xcodeproj/xcshareddata/ 78 | Example/AltSwiftUIExample.xcodeproj/project.xcworkspace/xcuserdata/ 79 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/Published.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Published.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol ChildWrapper { 12 | func setParent(_ parent: AnyObject) 13 | } 14 | 15 | /// A type that publishes a property. 16 | /// 17 | /// When the wrapped value of a published type changes, views that have 18 | /// accessed to its parent `ObservableObject` through a `@ObservedObject` 19 | /// property, will receive render updates. 20 | /// 21 | /// - Important: Published types should be used only in `ObservableObject` classes. 22 | @propertyWrapper public class Published: ChildWrapper { 23 | var _wrappedValue: Value 24 | private weak var parent: AnyObject? 25 | 26 | public init(wrappedValue value: Value) { 27 | _wrappedValue = value 28 | } 29 | 30 | /// The internal value of this wrapper type. 31 | public var wrappedValue: Value { 32 | get { 33 | _wrappedValue 34 | } 35 | set { 36 | let oldValue = _wrappedValue 37 | _wrappedValue = newValue 38 | if let hashOld = oldValue as? AnyHashable, let hashNew = newValue as? AnyHashable, hashOld == hashNew { 39 | return 40 | } 41 | if EnvironmentHolder.notifyStateChanges { 42 | sendStateChangeNotification() 43 | } 44 | } 45 | } 46 | 47 | /// The value as accessing $foo on a @Published property. 48 | public var projectedValue: Published { 49 | self 50 | } 51 | 52 | func setParent(_ parent: AnyObject) { 53 | self.parent = parent 54 | } 55 | 56 | private func sendStateChangeNotification() { 57 | if let parent = parent { 58 | let userInfo = EnvironmentHolder.notificationUserInfo 59 | NotificationCenter.default.post(name: ViewBinder.StateNotification.name, object: parent, userInfo: userInfo) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Shapes/Ellipse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ellipse.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Nodehi, Jabbar on 2020/09/09. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that represents a Ellipse shape. 12 | public struct Ellipse: Shape { 13 | 14 | public var viewStore = ViewValues() 15 | 16 | public var fillColor = Color.clear 17 | public var strokeBorderColor = Color.clear 18 | public var style = StrokeStyle() 19 | 20 | public var body: View { 21 | EmptyView() 22 | } 23 | 24 | public init() {} 25 | 26 | public func createView(context: Context) -> UIView { 27 | let view = AltShapeView().noAutoresizingMask() 28 | view.updateOnLayout = { [weak view] rect in 29 | guard let view = view else { return } 30 | updatePath(view: view, path: path(from: rect), animation: nil) 31 | } 32 | updateView(view, context: context.withoutTransaction) 33 | return view 34 | } 35 | 36 | public func updateView(_ view: UIView, context: Context) { 37 | guard let view = view as? AltShapeView else { return } 38 | let oldView = view.lastRenderableView?.view as? Ellipse 39 | 40 | let width = context.viewValues?.viewDimensions?.width ?? view.bounds.width 41 | let height = context.viewValues?.viewDimensions?.height ?? view.bounds.height 42 | let animation = context.transaction?.animation 43 | view.lastSizeFromViewUpdate = CGSize(width: width, height: height) 44 | 45 | if context.viewValues?.viewDimensions != oldView?.viewStore.viewDimensions { 46 | updatePath(view: view, path: path(from: CGRect(x: 0, y: 0, width: width, height: height)), animation: animation) 47 | } 48 | updateShapeLayerValues(view: view, context: context) 49 | } 50 | 51 | private func path(from rect: CGRect) -> UIBezierPath { 52 | UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: rect.width, height: rect.height)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/ObservedObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObservedObject.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Used to observe an `ObservableObject` when changes to its 12 | /// `Published` properties occur. 13 | @propertyWrapper 14 | public class ObservedObject { 15 | /// A wrapper of the underlying `ObservableObject` that can create 16 | /// `Binding`s to its properties using dynamic member lookup. 17 | @dynamicMemberLookup public struct Wrapper { 18 | 19 | var parent: ObservedObject 20 | 21 | /// Creates a `Binding` to a value semantic property of a 22 | /// reference type. 23 | /// 24 | /// If `Value` is not value semantic, the updating behavior for 25 | /// any views that make use of the resulting `Binding` is 26 | /// unspecified. 27 | public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Binding { 28 | Binding(get: { self.parent.wrappedValue[keyPath: keyPath] }, 29 | set: { self.parent.wrappedValue[keyPath: keyPath] = $0 }) 30 | } 31 | } 32 | 33 | var _wrappedValue: ObjectType 34 | 35 | public init(wrappedValue value: ObjectType) { 36 | _wrappedValue = value 37 | value.setupPublishedValues() 38 | } 39 | 40 | /// The internal value of this wrapper type. 41 | public var wrappedValue: ObjectType { 42 | get { 43 | EnvironmentHolder.currentBodyViewBinderStack.last?.registerStateNotification(origin: _wrappedValue) 44 | return _wrappedValue 45 | } 46 | set { 47 | _wrappedValue = newValue 48 | } 49 | } 50 | 51 | /// The direct value of this wrapper, as accessing $foo on a @EnvironmentObject property. 52 | public var projectedValue: ObservedObject.Wrapper { 53 | Wrapper(parent: self) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Shapes/Capsule.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Capsule.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Nodehi, Jabbar on 2020/09/09. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that represents a Capsule shape. 12 | public struct Capsule: Shape { 13 | public var viewStore = ViewValues() 14 | 15 | public var fillColor = Color.clear 16 | public var strokeBorderColor = Color.clear 17 | public var style = StrokeStyle() 18 | 19 | public var body: View { 20 | EmptyView() 21 | } 22 | 23 | public init() {} 24 | 25 | public func createView(context: Context) -> UIView { 26 | let view = AltShapeView().noAutoresizingMask() 27 | view.updateOnLayout = { [weak view] rect in 28 | guard let view = view else { return } 29 | updatePath(view: view, path: path(from: rect), animation: nil) 30 | } 31 | updateView(view, context: context.withoutTransaction) 32 | return view 33 | } 34 | 35 | public func updateView(_ view: UIView, context: Context) { 36 | guard let view = view as? AltShapeView else { return } 37 | let oldView = view.lastRenderableView?.view as? Capsule 38 | 39 | let width = context.viewValues?.viewDimensions?.width ?? view.bounds.width 40 | let height = context.viewValues?.viewDimensions?.height ?? view.bounds.height 41 | let animation = context.transaction?.animation 42 | view.lastSizeFromViewUpdate = CGSize(width: width, height: height) 43 | 44 | if context.viewValues?.viewDimensions != oldView?.viewStore.viewDimensions { 45 | updatePath(view: view, path: path(from: CGRect(x: 0, y: 0, width: width, height: height)), animation: animation) 46 | } 47 | updateShapeLayerValues(view: view, context: context) 48 | } 49 | 50 | private func path(from rect: CGRect) -> UIBezierPath { 51 | UIBezierPath( 52 | roundedRect: CGRect(x: 0, y: 0, width: rect.width, height: rect.height), 53 | cornerRadius: min(rect.width, rect.height)/2 54 | ) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/LazyVStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyVStack.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin | Kevs | TDD on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Arranges subviews vertically. If inside a `ScrollView`, subviews will be 11 | /// lazy loaded. 12 | /// 13 | /// __Important__: There should only be one `LazyVStack` per `ScrollView`. 14 | /// If there are more than one, only the first one will be lazy loaded. 15 | /// 16 | /// This view expands its width dimension by default. 17 | /// 18 | /// It's not recommended to use this view outside a `ScrollView` as there is no 19 | /// lazy loading benefit and there will be some extra overhead as compared to using a 20 | /// `VStack`. 21 | public struct LazyVStack: LazyStack, View { 22 | public var viewStore = ViewValues() 23 | 24 | let viewContentBuilder: () -> View 25 | let alignment: HorizontalAlignment 26 | let spacing: CGFloat 27 | var noPropertiesStack: Stack 28 | 29 | public var body: View { 30 | EmptyView() 31 | } 32 | var scrollAxis: Axis { .vertical } 33 | var stackAxis: NSLayoutConstraint.Axis { .vertical } 34 | 35 | /// Creates an instance of a view that arranges subviews vertically. If inside 36 | /// a `ScrollView`, subviews will be lazy loaded. 37 | /// 38 | /// - Parameters: 39 | /// - alignment: The horizontal alignment guide for its children. Defaults to `center`. 40 | /// - spacing: The vertical distance between subviews. If not specified, 41 | /// the distance will be 0. 42 | /// - content: A view builder that creates the content of this stack. 43 | public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: @escaping () -> View) { 44 | noPropertiesStack = VStack(alignment: alignment, spacing: spacing, content: content) 45 | viewContentBuilder = content 46 | self.alignment = alignment 47 | self.spacing = spacing ?? 0 48 | viewStore.direction = .vertical 49 | } 50 | 51 | func updateStackAlignment(stack: SwiftUILazyStackView) { 52 | stack.setStackAlignment(alignment: alignment) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/LazyHStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyHStack.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin | Kevs | TDD on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Arranges subviews horizontally. If inside a `ScrollView`, subviews will be 11 | /// lazy loaded. 12 | /// 13 | /// __Important__: There should only be one `LazyHStack` per `ScrollView`. 14 | /// If there are more than one, only the first one will be lazy loaded. 15 | /// 16 | /// This view expands its width dimension by default. 17 | /// 18 | /// It's not recommended to use this view outside a `ScrollView` as there is no 19 | /// lazy loading benefit and there will be some extra overhead as compared to using a 20 | /// `HStack`. 21 | public struct LazyHStack: LazyStack, View { 22 | public var viewStore = ViewValues() 23 | 24 | let viewContentBuilder: () -> View 25 | let alignment: VerticalAlignment 26 | let spacing: CGFloat 27 | var noPropertiesStack: Stack 28 | 29 | public var body: View { 30 | EmptyView() 31 | } 32 | var scrollAxis: Axis { .horizontal } 33 | var stackAxis: NSLayoutConstraint.Axis { .horizontal } 34 | 35 | /// Creates an instance of a view that arranges subviews horizontally. If inside 36 | /// a `ScrollView`, subviews will be lazy loaded. 37 | /// 38 | /// - Parameters: 39 | /// - alignment: The horizontal alignment guide for its children. Defaults to `center`. 40 | /// - spacing: The vertical distance between subviews. If not specified, 41 | /// the distance will be 0. 42 | /// - content: A view builder that creates the content of this stack. 43 | public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: @escaping () -> View) { 44 | noPropertiesStack = HStack(alignment: alignment, spacing: spacing, content: content) 45 | viewContentBuilder = content 46 | self.alignment = alignment 47 | self.spacing = spacing ?? 0 48 | viewStore.direction = .horizontal 49 | } 50 | 51 | func updateStackAlignment(stack: SwiftUILazyStackView) { 52 | stack.setStackAlignment(alignment: alignment) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Shapes/RoundedRectangle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedRectangle.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Nodehi, Jabbar on 2020/09/09. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that represents a RoundedRectangle shape. 12 | public struct RoundedRectangle: Shape { 13 | public var viewStore = ViewValues() 14 | 15 | public var fillColor = Color.clear 16 | public var strokeBorderColor = Color.clear 17 | public var style = StrokeStyle() 18 | public var cornerRadius: CGFloat 19 | 20 | public var body: View { 21 | EmptyView() 22 | } 23 | 24 | public init(cornerRadius: CGFloat) { 25 | self.cornerRadius = cornerRadius 26 | } 27 | 28 | public func createView(context: Context) -> UIView { 29 | let view = AltShapeView().noAutoresizingMask() 30 | view.updateOnLayout = { [weak view] rect in 31 | guard let view = view else { return } 32 | updatePath(view: view, path: path(from: rect), animation: nil) 33 | } 34 | updateView(view, context: context.withoutTransaction) 35 | return view 36 | } 37 | 38 | public func updateView(_ view: UIView, context: Context) { 39 | guard let view = view as? AltShapeView else { return } 40 | let oldView = view.lastRenderableView?.view as? RoundedRectangle 41 | 42 | let width = context.viewValues?.viewDimensions?.width ?? view.bounds.width 43 | let height = context.viewValues?.viewDimensions?.height ?? view.bounds.height 44 | let animation = context.transaction?.animation 45 | view.lastSizeFromViewUpdate = CGSize(width: width, height: height) 46 | 47 | if context.viewValues?.viewDimensions != oldView?.viewStore.viewDimensions { 48 | updatePath(view: view, path: path(from: CGRect(x: 0, y: 0, width: width, height: height)), animation: animation) 49 | } 50 | updateShapeLayerValues(view: view, context: context) 51 | } 52 | 53 | private func path(from rect: CGRect) -> UIBezierPath { 54 | UIBezierPath( 55 | roundedRect: CGRect(x: 0, y: 0, width: rect.width, height: rect.height), 56 | cornerRadius: cornerRadius 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2019/08/26. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AltSwiftUI 11 | import protocol AltSwiftUI.ObservableObject 12 | import class AltSwiftUI.Published 13 | 14 | struct ExampleViewData { 15 | var title: String 16 | var destination: View 17 | } 18 | 19 | struct ExampleView: View { 20 | var viewStore = ViewValues() 21 | 22 | var views: [ExampleViewData] = [ 23 | ExampleViewData(title: "2 Axis Scroll", destination: ScrollView2AxisExampleView()), 24 | ExampleViewData(title: "Alerts", destination: AlertsExampleView()), 25 | ExampleViewData(title: "LazyStack", destination: LazyStackExampleView()), 26 | ExampleViewData(title: "List", destination: ListExampleView()), 27 | ExampleViewData(title: "List + TextField", destination: ListTextFieldExampleView()), 28 | ExampleViewData(title: "Menu", destination: MenuExampleView()), 29 | ExampleViewData(title: "Navigation", destination: NavigationExampleView()), 30 | ExampleViewData(title: "Ramen Example", destination: RamenExampleView()), 31 | ExampleViewData(title: "ScrollView + TextField", destination: ScrollViewTextFieldExampleView()), 32 | ExampleViewData(title: "SecureField", destination: SecureFieldExampleView()), 33 | ExampleViewData(title: "Shapes", destination: ShapesExampleView()), 34 | ExampleViewData(title: "Stack Update", destination: StackUpdateExample()), 35 | ExampleViewData(title: "Texts", destination: TextExampleView()), 36 | ExampleViewData(title: "TextField", destination: TextFieldExampleView()) 37 | ] 38 | 39 | var body: View { 40 | NavigationView { 41 | List(views, id: \ExampleViewData.title) { view in 42 | NavigationLink(destination: view.destination) { 43 | Text("\(view.title)") 44 | .multilineTextAlignment(.leading) 45 | .frame(height: 40) 46 | .frame(maxWidth: .infinity) 47 | } 48 | } 49 | } 50 | .navigationBarTitle("AltSwiftUI Examples", displayMode: .inline) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Previews/AltPreviewProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AltPreviewProvider.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/01/15. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | // MARK: - Public Types 13 | 14 | /// A type that produces AltSwiftUI view previews in Xcode. 15 | /// 16 | /// Xcode statically discovers types that conform to the ``AltPreviewProvider`` 17 | /// protocol in your app, and generates previews for each provider it discovers. 18 | /// 19 | /// __Note__: In case you can't get the preview window to show, make sure 20 | /// `Editor -> Canvas` is enabled. Additionally, you can access another file 21 | /// whose canvas is properly showing, and then pin it so that it is visible 22 | /// in all files. 23 | @available(iOS 13.0.0, *) 24 | public protocol AltPreviewProvider: PreviewProvider { 25 | /// Generates a preview of one view. 26 | /// 27 | /// The following code shows how to create a preview provider for previewing 28 | /// a `MyText` view: 29 | /// 30 | /// #if DEBUG && canImport(SwiftUI) 31 | /// 32 | /// import protocol SwiftUI.PreviewProvider 33 | /// import protocol RakutenTravelCore.View 34 | /// 35 | /// struct MyTextPreview : AltPreviewProvider, PreviewProvider { 36 | /// static var previewView: View { 37 | /// MyText() 38 | /// } 39 | /// } 40 | /// 41 | /// #endif 42 | static var previewView: View { get } 43 | } 44 | 45 | @available(iOS 13.0.0, *) 46 | public extension AltPreviewProvider { 47 | static var previews: some SwiftUI.View { 48 | PreviewProviderViewCRepresentable(contentView: previewView) 49 | } 50 | } 51 | 52 | // MARK: - Private Types 53 | 54 | typealias RakuContext = Context 55 | 56 | @available(iOS 13.0, *) 57 | struct PreviewProviderViewCRepresentable: SwiftUI.UIViewControllerRepresentable { 58 | public typealias UIViewControllerType = UIViewController 59 | 60 | public func makeUIViewController(context: SwiftUI.UIViewControllerRepresentableContext) -> UIViewController { 61 | UIHostingController(rootView: contentView) 62 | } 63 | 64 | public func updateUIViewController(_ uiViewController: UIViewController, context: SwiftUI.UIViewControllerRepresentableContext) { 65 | } 66 | 67 | let contentView: View 68 | } 69 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/State.swift: -------------------------------------------------------------------------------- 1 | // 2 | // State.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2019/10/07. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class StateValueHolder { 12 | var value: Value 13 | init(value: Value) { 14 | self.value = value 15 | } 16 | } 17 | 18 | /// Represents a mutable value to be held by a `View`. Reading a state's 19 | /// `wrappedValue` property inside a view's `body` computed property will 20 | /// trigger a subscribption by the view to listen to changes in the state's value. 21 | /// 22 | /// - Important: You shouldn't modify a state's wrapped value when the view's 23 | /// `body` property is being read. 24 | @propertyWrapper 25 | public class State { 26 | var _wrappedValue: StateValueHolder 27 | 28 | public init(wrappedValue value: Value) { 29 | _wrappedValue = StateValueHolder(value: value) 30 | } 31 | 32 | /// The internal value of this wrapper type. 33 | public var wrappedValue: Value { 34 | get { 35 | EnvironmentHolder.currentBodyViewBinderStack.last?.registerStateNotification(origin: _wrappedValue) 36 | return _wrappedValue.value 37 | } 38 | set { 39 | let oldValue = _wrappedValue.value 40 | _wrappedValue.value = newValue 41 | if let hashOld = oldValue as? AnyHashable, let hashNew = newValue as? AnyHashable, hashOld == hashNew { 42 | return 43 | } 44 | if EnvironmentHolder.notifyStateChanges { 45 | sendStateChangeNotification() 46 | } 47 | } 48 | } 49 | 50 | /// The direct value of this wrapper, as accessing $foo on a @State property. 51 | public var projectedValue: Binding { 52 | Binding( 53 | get: { self.wrappedValue }, 54 | set: { self.wrappedValue = $0 } 55 | ) 56 | } 57 | 58 | private func sendStateChangeNotification() { 59 | let userInfo = EnvironmentHolder.notificationUserInfo 60 | NotificationCenter.default.post(name: ViewBinder.StateNotification.name, object: _wrappedValue, userInfo: userInfo) 61 | } 62 | } 63 | 64 | extension State: MigratableProperty { 65 | var internalValue: Any { 66 | _wrappedValue 67 | } 68 | func setInternalValue(_ value: Any) { 69 | if let value = value as? StateValueHolder { 70 | _wrappedValue = value 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Submit an Issue 4 | Submit an issue for bugs and improvement/feature requests. Please fill the following information in each issue you submit: 5 | 6 | * Title: Use a clear and descriptive title for the issue to identify the problem. 7 | * Description: Description of the issue. 8 | * Scenario/Steps to Reproduce: numbered step by step. (1,2,3.… and so on) 9 | * Expected behaviour: What you expect to happen. 10 | * Actual behaviour (for bugs): What actually happens. 11 | * How often reproduces? (for bugs): what percentage of the time does it reproduce? 12 | * Version: the version of the library. 13 | * Operating system: The operating system used. 14 | * Additional information: Any additional to help to reproduce. (screenshots, animated gifs) 15 | 16 | ## Pull Requests 17 | 1. Fork the project 18 | 2. Submit a pull request to `master` branch with the following information: 19 | 20 | * Title: Add a summary of what this pull request accomplishes 21 | * Description: Descibes the motivation and further details of this pull request 22 | * Issues: **Important!** Link existing issues that this pull request will close (if any) by using one of the [supported keywords / manually](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue) 23 | 24 | ## Coding Guidelines 25 | * All public types and methods must be documented 26 | * Code should follow the [coding guidelines](https://github.com/RakutenTravel/ios-coding-guidelines). 27 | 28 | ## Commit messages 29 | Each commit message consists of a header and a body. 30 | 31 | ``` 32 |
33 | 34 | 35 | ``` 36 | 37 | The **header** is mandatory. 38 | 39 | Any line of the commit message cannot be longer 100 characters! This allows the message to be easier 40 | to read on GitHub as well as in various git tools. 41 | 42 | ### Revert 43 | If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted. 44 | 45 | ### Breaking Changes 46 | 47 | When a commit has **Breaking Changes**, the **header** should be prefixed by the keyword `BREAKING:`. 48 | 49 | ### Header 50 | The header contains succinct description of the change: 51 | 52 | * use the imperative, present tense: "change" not "changed" nor "changes" 53 | * don't capitalize first letter 54 | * no dot (.) at the end 55 | 56 | ### Body 57 | Just as in the **header**, use the imperative, present tense: "change" not "changed" nor "changes". 58 | The body should include the motivation for the change and contrast this with previous behavior. -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/StateObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StateObject.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/25. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// StateObject works similarly as ObservedObject, with the only 12 | /// difference that for being part of a view's state, if the view 13 | /// is recreated while owning an object, the object won't be recreated 14 | /// and will keep it's previous data. 15 | /// 16 | /// On view recreation (by a state change in its parent): 17 | /// - ObservedObject owned by view: Gets recreated (re-initialized) 18 | /// - StateObject owned by view: Keeps same instance 19 | @propertyWrapper 20 | public class StateObject { 21 | /// A wrapper of the underlying `ObservableObject` that can create 22 | /// `Binding`s to its properties using dynamic member lookup. 23 | @dynamicMemberLookup public struct Wrapper { 24 | 25 | var parent: StateObject 26 | 27 | /// Creates a `Binding` to a value semantic property of a 28 | /// reference type. 29 | /// 30 | /// If `Value` is not value semantic, the updating behavior for 31 | /// any views that make use of the resulting `Binding` is 32 | /// unspecified. 33 | public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Binding { 34 | Binding(get: { self.parent.wrappedValue[keyPath: keyPath] }, 35 | set: { self.parent.wrappedValue[keyPath: keyPath] = $0 }) 36 | } 37 | } 38 | 39 | var _wrappedValue: ObjectType 40 | 41 | public init(wrappedValue value: ObjectType) { 42 | _wrappedValue = value 43 | value.setupPublishedValues() 44 | } 45 | 46 | /// The internal value of this wrapper type. 47 | public var wrappedValue: ObjectType { 48 | get { 49 | EnvironmentHolder.currentBodyViewBinderStack.last?.registerStateNotification(origin: _wrappedValue) 50 | return _wrappedValue 51 | } 52 | set { 53 | _wrappedValue = newValue 54 | } 55 | } 56 | 57 | /// The direct value of this wrapper, as accessing $foo on a @EnvironmentObject property. 58 | public var projectedValue: StateObject.Wrapper { 59 | Wrapper(parent: self) 60 | } 61 | } 62 | 63 | extension StateObject: MigratableProperty { 64 | var internalValue: Any { 65 | _wrappedValue 66 | } 67 | func setInternalValue(_ value: Any) { 68 | if let value = value as? ObjectType { 69 | _wrappedValue = value 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/State/EnvironmentObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentObject.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Used to reference and observe environment objects previously set. Environment 12 | /// objects are identified by their __type__, meaning only one of the same type can 13 | /// exist at a time as an environment object. 14 | /// 15 | /// - Important: Referencing an environment object not previously set 16 | /// will trigger an exception. 17 | @propertyWrapper public class EnvironmentObject: DynamicProperty where ObjectType: ObservableObject { 18 | /// A wrapper of the underlying `ObservableObject` that can create 19 | /// `Binding`s to its properties using dynamic member lookup. 20 | @dynamicMemberLookup public struct Wrapper { 21 | 22 | var parent: EnvironmentObject 23 | 24 | /// Creates a `Binding` to a value semantic property of a 25 | /// reference type. 26 | /// 27 | /// If `Value` is not value semantic, the updating behavior for 28 | /// any views that make use of the resulting `Binding` is 29 | /// unspecified. 30 | public subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Binding { 31 | Binding(get: { self.parent.wrappedValue[keyPath: keyPath] }, 32 | set: { self.parent.wrappedValue[keyPath: keyPath] = $0 }) 33 | } 34 | } 35 | 36 | var _wrappedValue: ObjectType? 37 | 38 | public init() { 39 | } 40 | 41 | /// The internal value of this wrapper type. 42 | public var wrappedValue: ObjectType { 43 | get { 44 | assert(_wrappedValue != nil, "EnvironmentObject being called outside of body") 45 | EnvironmentHolder.currentBodyViewBinderStack.last?.registerStateNotification(origin: _wrappedValue!) 46 | return _wrappedValue! 47 | } 48 | set { 49 | } 50 | } 51 | 52 | /// The direct value of this wrapper, as accessing $foo on a @EnvironmentObject property. 53 | public var projectedValue: EnvironmentObject.Wrapper { 54 | Wrapper(parent: self) 55 | } 56 | 57 | func update(context: Context) { 58 | if _wrappedValue != nil { 59 | return 60 | } 61 | 62 | if let envObject = EnvironmentHolder.environmentObjects[String(describing: ObjectType.self)] as? ObjectType { 63 | _wrappedValue = envObject 64 | _wrappedValue?.setupPublishedValues() 65 | } else { 66 | assertionFailure("Environment object of type \(ObjectType.self) should be set") 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/TabView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabView.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/05. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A tab view that is shown at the bottom of the screen. 12 | /// 13 | /// __Important__: This view will add a tab view at the __root__ 14 | /// screen in the application. 15 | @available(watchOS, unavailable) 16 | public struct TabView: View { 17 | let selection: Binding? 18 | let content: [View] 19 | private var selectedIndex: Int? { 20 | guard let selectionValue = selection?.wrappedValue else { 21 | return nil 22 | } 23 | return content.firstIndex { $0.viewStore.tag == selectionValue } 24 | } 25 | 26 | public var viewStore = ViewValues() 27 | public var body: View { 28 | EmptyView() 29 | } 30 | 31 | /// Creates an instance that selects from content associated with 32 | /// `Selection` values. 33 | public init(selection: Binding? = nil, @ViewBuilder content: () -> View) { 34 | self.selection = selection 35 | self.content = content().subViews 36 | } 37 | } 38 | 39 | extension TabView: Renderable { 40 | public func createView(context: Context) -> UIView { 41 | let controller = UIHostingController.customRootTabBarController 42 | var viewControllers = [UIViewController]() 43 | for (index, view) in content.enumerated() { 44 | var modifiedView = view 45 | if view.viewStore.tag == nil { 46 | modifiedView.viewStore.tag = index 47 | } 48 | let screenController = ScreenViewController(contentView: modifiedView, parentContext: context) 49 | viewControllers.append(UIHostingController(rootViewController: screenController)) 50 | } 51 | controller.viewControllers = viewControllers 52 | updateController(controller) 53 | UIApplication.shared.activeWindow?.rootViewController = controller 54 | return controller.view 55 | } 56 | 57 | public func updateView(_ view: UIView, context: Context) { 58 | guard let tabController = context.overwriteRootController as? SwiftUITabBarController else { return } 59 | updateController(tabController) 60 | } 61 | 62 | private func updateController(_ controller: SwiftUITabBarController) { 63 | controller.selectionChanged = { index in 64 | if self.content.count > index, let tag = self.content[index].viewStore.tag { 65 | self.selection?.wrappedValue = tag 66 | } 67 | } 68 | if let selectedIndex = self.selectedIndex, controller.currentSelectedIndex != selectedIndex { 69 | controller.selectedIndex = selectedIndex 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/AlertsExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertsExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Tanabe, Alex | Rx | TID on 2021/02/01. 6 | // Copyright © 2021 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | import UIKit 11 | 12 | struct AlertsExampleView: View { 13 | var viewStore = ViewValues() 14 | @State var foregroundDisplayNeeded = false 15 | @State var showAlert = false 16 | @State var showActionSheet = false 17 | @State var showSheet = false 18 | 19 | var body: View { 20 | VStack(spacing: 8) { 21 | Button("Show modal sheet") { 22 | showSheet = true 23 | } 24 | 25 | Button("Show alert from this view") { 26 | showAlert = true 27 | } 28 | 29 | Button("Show action sheet from this view") { 30 | showActionSheet = true 31 | } 32 | 33 | HStack { 34 | Toggle(isOn: $foregroundDisplayNeeded) { 35 | Text("Show Alerts from foreground view") 36 | .font(.system(size: 12)) 37 | } 38 | Spacer() 39 | } 40 | } 41 | .padding(.horizontal, 16.0) 42 | .sheet(isPresented: $showSheet) { 43 | VStack(spacing: 8) { 44 | Button("Show alert from prev. screen") { 45 | showAlert = true 46 | } 47 | 48 | Button("Show action sheet from prev. screen") { 49 | showActionSheet = true 50 | } 51 | 52 | Text("Alerts/Action sheets are shown from previous screen") 53 | .font(.system(size: 12)) 54 | .frame(maxWidth: .infinity) 55 | .padding(.top, 16) 56 | } 57 | } 58 | .alert(isPresented: $showAlert, onForegroundView: foregroundDisplayNeeded) { 59 | Alert( 60 | title: Text("Alert!"), 61 | message: Text("Dismiss me please :("), 62 | dismissButton: .default( 63 | Text("OK"), 64 | action: { 65 | showAlert = false 66 | } 67 | ) 68 | ) 69 | } 70 | .actionSheet(isPresented: $showActionSheet, onForegroundView: foregroundDisplayNeeded) { 71 | ActionSheet( 72 | title: Text("Action Sheet!"), 73 | message: Text("Dismiss me please :("), 74 | buttons: [.default( 75 | Text("OK"), 76 | action: { 77 | showActionSheet = false 78 | } 79 | )] 80 | ) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/ListExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ListExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2020/09/28. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | 11 | struct ListExampleView: View { 12 | var viewStore = ViewValues() 13 | @StateObject var ramenModel = RamenModel() 14 | @State var scrollEnabled = true 15 | @State private var appliedRow: ListVisibleRow? 16 | @State private var interactiveScrollEnabled = true 17 | 18 | var body: View { 19 | VStack { 20 | HStack { 21 | Button("Remove") { 22 | withAnimation { 23 | _ = ramenModel.ramenList.remove(at: 3) 24 | } 25 | } 26 | Button("Add") { 27 | withAnimation { 28 | ramenModel.ramenList.insert(Ramen(id: UUID().uuidString, name: "Insert Ramen", score: 3, price: "20"), at: 3) 29 | } 30 | } 31 | Button("Update") { 32 | withAnimation { 33 | ramenModel.ramenList[3].name = "Updated Ramen" 34 | ramenModel.ramenList[3].price = "100" 35 | } 36 | } 37 | Button("Toggle") { 38 | withAnimation { 39 | if ramenModel.ramenList[3].score == 6 { 40 | ramenModel.ramenList[3].score = 5 41 | } else { 42 | ramenModel.ramenList[3].score = 6 43 | } 44 | } 45 | } 46 | Button("Int. Lock") { 47 | interactiveScrollEnabled.toggle() 48 | } 49 | Button("Visible") { 50 | withAnimation { 51 | appliedRow = ListVisibleRow(indexPath: IndexPath(row: 0, section: 0), scrollPosition: .top) 52 | } 53 | } 54 | } 55 | HStack { 56 | Button("Lock/Unlock Scroll") { 57 | scrollEnabled.toggle() 58 | } 59 | } 60 | 61 | List(ramenModel.ramenList) { ramen in 62 | RamenCell(ramen: ramen) 63 | } 64 | .listStyle(listStyle) 65 | .scrollEnabled(scrollEnabled) 66 | .interactiveScrollEnabled(interactiveScrollEnabled) 67 | .appliedVisibleRow($appliedRow) 68 | } 69 | .onAppear { 70 | ramenModel.loadRamen() 71 | } 72 | } 73 | 74 | var listStyle: ListStyle { 75 | if #available(iOS 13.0, *) { 76 | return InsetGroupedListStyle() 77 | } else { 78 | return PlainListStyle() 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/StackUpdateExample.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StackUpdateExample.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2021/01/29. 6 | // Copyright © 2021 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | 11 | struct StackUpdateExample: View { 12 | var viewStore = ViewValues() 13 | 14 | @State private var update = false 15 | 16 | var body: View { 17 | HStack(alignment: .top) { 18 | VStack { 19 | Text("Target Stack") 20 | .font(Font.body.weight(.bold)) 21 | Text("First text") 22 | Button("First button") { 23 | print("First button print") 24 | } 25 | VStack { 26 | Text("Text in Stack") 27 | } 28 | .background(.red) 29 | StackUpdateSubview() 30 | } 31 | .frame(maxWidth: .infinity) 32 | 33 | VStack { 34 | HStack { 35 | Text("Stack To Update") 36 | .font(Font.body.weight(.bold)) 37 | Button("Update") { 38 | update.toggle() 39 | } 40 | } 41 | if update { 42 | Text("First text") 43 | } else { 44 | Text("Pre: First text") 45 | .background(.blue) 46 | } 47 | if update { 48 | Button("First button") { 49 | print("First button print") 50 | } 51 | } else { 52 | Button("Pre: First button") { 53 | print("Pre: First button print") 54 | } 55 | } 56 | if update { 57 | VStack { 58 | Text("Text in Stack") 59 | } 60 | .background(.red) 61 | } else { 62 | Text("Shouldn't be here") 63 | Text("Shouldn't be here either") 64 | ZStack { 65 | Text("Much less be here") 66 | } 67 | VStack { 68 | Text("Pre: Text in Stack") 69 | } 70 | .background(.green) 71 | } 72 | if update { 73 | StackUpdateSubview() 74 | } else { 75 | StackUpdateSubview() 76 | } 77 | } 78 | .frame(maxWidth: .infinity) 79 | } 80 | } 81 | } 82 | 83 | private struct StackUpdateSubview: View { 84 | var viewStore = ViewValues() 85 | @State private var title = "Press before update" 86 | var body: View { 87 | VStack { 88 | Button("\(title)") { 89 | title = "Shouldn't show after update" 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Controls/Slider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Slider.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that displays a range of values, and a knob that allows selecting 12 | /// a value in the range. 13 | public struct Slider: View { 14 | public var viewStore = ViewValues() 15 | let value: Binding 16 | let bounds: ClosedRange 17 | var step: Float? 18 | public var body: View { 19 | EmptyView() 20 | } 21 | 22 | /// Creates an instance that selects a value from within a range. 23 | /// 24 | /// - Parameters: 25 | /// - value: The selected value within `bounds`. 26 | /// - bounds: The range of the valid values. Defaults to `0...1`. 27 | /// 28 | /// The `value` of the created instance will be equal to the position of 29 | /// the given value within `bounds`, mapped into `0...1`. 30 | @available(tvOS, unavailable) 31 | public init(value: Binding, in bounds: ClosedRange = 0...1) { 32 | self.value = value 33 | self.bounds = bounds 34 | } 35 | 36 | /// Creates an instance that selects a value from within a range. 37 | /// 38 | /// - Parameters: 39 | /// - value: The selected value within `bounds`. 40 | /// - bounds: The range of the valid values. Defaults to `0...1`. 41 | /// - step: The distance between each valid value. 42 | /// - label: A `View` that describes the purpose of the instance. 43 | /// 44 | /// The `value` of the created instance will be equal to the position of 45 | /// the given value within `bounds`, mapped into `0...1`. 46 | @available(tvOS, unavailable) 47 | public init(value: Binding, in bounds: ClosedRange, step: Float = 1) { 48 | self.value = value 49 | self.bounds = bounds 50 | if step != 0 { 51 | self.step = step 52 | } 53 | } 54 | } 55 | 56 | extension Slider: Renderable { 57 | public func createView(context: Context) -> UIView { 58 | let view = SwiftUISlider().noAutoresizingMask() 59 | updateView(view, context: context) 60 | return view 61 | } 62 | 63 | public func updateView(_ view: UIView, context: Context) { 64 | guard let view = view as? SwiftUISlider else { return } 65 | 66 | view.value = value.wrappedValue 67 | view.minimumValue = bounds.lowerBound 68 | view.maximumValue = bounds.upperBound 69 | view.valueChanged = { [weak view] in 70 | guard let view = view else { return } 71 | var value = view.value 72 | if let step = self.step { 73 | // Round the value up or down 74 | let remainder = value.remainder(dividingBy: step) 75 | if remainder > step / 2 { 76 | value += remainder 77 | } else { 78 | value += (remainder - step) 79 | } 80 | } 81 | self.value.wrappedValue = value 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/UIKitViews/UIKitNavigationViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitNavigationViews.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2019/10/09. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Public Types 12 | 13 | /// The default TabBarController used by AltSwiftUI. 14 | /// 15 | /// Subclass this class if you want to add custom behavior to 16 | /// the `UITabBarController` and add it to `UIHostingController.customRootTabBarController`. 17 | open class SwiftUITabBarController: UITabBarController, UITabBarControllerDelegate { 18 | var selectionChanged: ((Int) -> Void)? 19 | var currentSelectedIndex: Int = 0 20 | 21 | override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 22 | super.init(nibName: nil, bundle: nil) 23 | setupController() 24 | } 25 | public required init?(coder: NSCoder) { 26 | fatalError("init(coder:) has not been implemented") 27 | } 28 | 29 | override open var selectedIndex: Int { 30 | didSet { 31 | currentSelectedIndex = selectedIndex 32 | } 33 | } 34 | 35 | private func setupController() { 36 | delegate = self 37 | } 38 | 39 | // MARK: Delegate 40 | 41 | override open func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { 42 | if let index = tabBar.items?.firstIndex(of: item) { 43 | currentSelectedIndex = index 44 | selectionChanged?(index) 45 | } 46 | } 47 | } 48 | 49 | // MARK: - Internal Types 50 | 51 | class SwiftUIPresenter: NSObject, UIAdaptivePresentationControllerDelegate { 52 | let onDismiss: () -> Void 53 | 54 | init(onDismiss: @escaping () -> Void) { 55 | self.onDismiss = onDismiss 56 | } 57 | 58 | func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { 59 | onDismiss() 60 | } 61 | } 62 | 63 | class SwiftUIBarButtonItem: UIBarButtonItem { 64 | class ActionHolder { 65 | var buttonAction: () -> Void 66 | init(buttonAction: @escaping () -> Void) { 67 | self.buttonAction = buttonAction 68 | } 69 | @objc func performAction() { 70 | buttonAction() 71 | } 72 | } 73 | 74 | var actionHolder: ActionHolder? 75 | 76 | convenience init(title: String, style: UIBarButtonItem.Style, accent: UIColor? = nil, buttonAction: @escaping () -> Void) { 77 | let actionHolder = ActionHolder(buttonAction: buttonAction) 78 | self.init(title: title, style: style, target: actionHolder, action: #selector(ActionHolder.performAction)) 79 | self.actionHolder = actionHolder 80 | tintColor = accent 81 | } 82 | convenience init(image: UIImage, style: UIBarButtonItem.Style, accent: UIColor? = nil, buttonAction: @escaping () -> Void) { 83 | let actionHolder = ActionHolder(buttonAction: buttonAction) 84 | self.init(image: image, style: style, target: actionHolder, action: #selector(ActionHolder.performAction)) 85 | self.actionHolder = actionHolder 86 | tintColor = accent 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Example/AltSwiftUIExample/ExampleViews/ShapesExampleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShapesExampleView.swift 3 | // AltSwiftUIExample 4 | // 5 | // Created by Wong, Kevin a on 2020/09/24. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import AltSwiftUI 10 | import UIKit 11 | 12 | struct ShapesExampleView: View { 13 | var viewStore = ViewValues() 14 | @State private var goBig = false 15 | @State private var progress: CGFloat = 0.5 16 | 17 | var body: View { 18 | VStack(spacing: 10) { 19 | Button(action: { 20 | goBig.toggle() 21 | }, label: { 22 | Text("PRESS ME") 23 | }) 24 | .padding([.top, .bottom], 20) 25 | 26 | RoundedRectangle(cornerRadius: goBig ? 20 : 10) 27 | .background(.yellow) 28 | .fill(goBig ? .purple : .green) 29 | .strokeBorder(goBig ? .pink : .purple, lineWidth: goBig ? 8 : 3) 30 | .frame(width: goBig ? 300 : 200, height: 200) 31 | 32 | Button("Progress") { 33 | if progress == 0.5 { 34 | progress = 1 35 | } else { 36 | progress = 0.5 37 | } 38 | } 39 | HStack { 40 | Capsule() 41 | .frame(width: 50, height: 70) 42 | .fill(goBig ? .purple : .green) 43 | Circle() 44 | .frame(width: 50, height: 70) 45 | .fill(goBig ? .purple : .green) 46 | .stroke(Color.black, style: .init(lineWidth: 8, lineCap: .round)) 47 | Circle() 48 | .frame(width: 50, height: 70) 49 | .fill(goBig ? .purple : .green) 50 | .stroke(Color.black, style: .init(lineWidth: 8, lineCap: .round)) 51 | .trim(from: 0.25, to: 0.75) 52 | Circle() 53 | .frame(width: 100, height: 70) 54 | .stroke(Color.black, style: .init(lineWidth: 8, lineCap: .round)) 55 | .trim(from: 0.25, to: progress) 56 | .animation(.spring()) 57 | } 58 | 59 | HStack { 60 | Ellipse() 61 | .frame(width: 50, height: 70) 62 | .fill(goBig ? .purple : .green) 63 | Rectangle() 64 | .frame(width: 50, height: 70) 65 | .fill(goBig ? .purple : .green) 66 | } 67 | 68 | ShapeExampleNonShapeView(goBig: $goBig) 69 | } 70 | .animation(.easeIn(duration: 0.5)) 71 | } 72 | } 73 | 74 | struct ShapeExampleNonShapeView: View { 75 | var viewStore = ViewValues() 76 | @Binding var goBig: Bool 77 | var body: View { 78 | Text("Non shape") 79 | .frame(width: goBig ? 100 : 50, height: 100) 80 | .background(goBig ? .purple : .pink) 81 | .cornerRadius(goBig ? 20 : 5) 82 | .border(goBig ? .blue : .green, width: goBig ? 10 : 3) 83 | .padding(.top, 20) 84 | .onTapGesture { 85 | print("asd") 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/ViewProperties/ViewBuilder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewBuilder.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A parameter and function attribute that can specify multiple views in the 12 | /// form of a closure. 13 | /// 14 | /// ViewBuilder is used when passing children views as parameter to a parent 15 | /// view. 16 | @_functionBuilder 17 | public enum ViewBuilder { 18 | public static func buildBlock() -> EmptyView { 19 | EmptyView() 20 | } 21 | 22 | public static func buildBlock(_ children: View) -> View { 23 | children 24 | } 25 | 26 | public static func buildBlock(_ c0: View, _ c1: View) -> TupleView { 27 | TupleView([c0, c1]) 28 | } 29 | 30 | public static func buildBlock(_ c0: View, _ c1: View, _ c2: View) -> TupleView { 31 | TupleView([c0, c1, c2]) 32 | } 33 | 34 | public static func buildBlock(_ c0: View, _ c1: View, _ c2: View, _ c3: View) -> TupleView { 35 | TupleView([c0, c1, c2, c3]) 36 | } 37 | 38 | public static func buildBlock(_ c0: View, _ c1: View, _ c2: View, _ c3: View, _ c4: View) -> TupleView { 39 | TupleView([c0, c1, c2, c3, c4]) 40 | } 41 | 42 | public static func buildBlock(_ c0: View, _ c1: View, _ c2: View, _ c3: View, _ c4: View, _ c5: View) -> TupleView { 43 | TupleView([c0, c1, c2, c3, c4, c5]) 44 | } 45 | 46 | public static func buildBlock(_ c0: View, _ c1: View, _ c2: View, _ c3: View, _ c4: View, _ c5: View, _ c6: View) -> TupleView { 47 | TupleView([c0, c1, c2, c3, c4, c5, c6]) 48 | } 49 | 50 | public static func buildBlock(_ c0: View, _ c1: View, _ c2: View, _ c3: View, _ c4: View, _ c5: View, _ c6: View, _ c7: View) -> TupleView { 51 | TupleView([c0, c1, c2, c3, c4, c5, c6, c7]) 52 | } 53 | 54 | public static func buildBlock(_ c0: View, _ c1: View, _ c2: View, _ c3: View, _ c4: View, _ c5: View, _ c6: View, _ c7: View, _ c8: View) -> TupleView { 55 | TupleView([c0, c1, c2, c3, c4, c5, c6, c7, c8]) 56 | } 57 | 58 | public static func buildBlock(_ c0: View, _ c1: View, _ c2: View, _ c3: View, _ c4: View, _ c5: View, _ c6: View, _ c7: View, _ c8: View, _ c9: View) -> TupleView { 59 | TupleView([c0, c1, c2, c3, c4, c5, c6, c7, c8, c9]) 60 | } 61 | 62 | /// Provides support for "if" statements in multi-statement closures, producing an `Optional` view 63 | /// that is visible only when the `if` condition evaluates `true`. 64 | public static func buildIf(_ content: View?) -> OptionalView { 65 | OptionalView(content: content?.subViews) 66 | } 67 | 68 | /// Provides support for "if" statements in multi-statement closures, producing 69 | /// ConditionalContent for the "then" branch. 70 | public static func buildEither(first: View) -> View { 71 | OptionalView(content: first.subViews, ifElseType: .if) 72 | } 73 | 74 | /// Provides support for "if-else" statements in multi-statement closures, producing 75 | /// ConditionalContent for the "else" branch. 76 | public static func buildEither(second: View) -> View { 77 | OptionalView(content: second.subViews, ifElseType: .else) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/AltSwiftUITests/ViewBinderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewBinderTests.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/10/08. 6 | // 7 | 8 | import XCTest 9 | @testable import AltSwiftUI 10 | @testable import protocol AltSwiftUI.ObservableObject 11 | @testable import class AltSwiftUI.Published 12 | 13 | class ViewBinderTests: XCTestCase { 14 | class ViewBinderTestObject: ObservableObject { 15 | @Published var change = "Object" 16 | } 17 | struct ViewBinderStateTestView: View { 18 | var viewStore = ViewValues() 19 | @State var state = "test" 20 | var body: View { Text("\(state)") } 21 | } 22 | struct ViewBinderBindingTestView: View { 23 | var viewStore = ViewValues() 24 | @Binding var state: String 25 | var body: View { Text("\(state)") } 26 | } 27 | struct ViewBinderObservedObjTestView: View { 28 | var viewStore = ViewValues() 29 | @ObservedObject var object = ViewBinderTestObject() 30 | var body: View { Text("\(object.change)") } 31 | } 32 | struct ViewBinderStateObjTestView: View { 33 | var viewStore = ViewValues() 34 | @StateObject var object = ViewBinderTestObject() 35 | var body: View { Text("\(object.change)") } 36 | } 37 | struct ViewBinderEnvironmentObjTestView: View { 38 | var viewStore = ViewValues() 39 | var object: EnvironmentObject = { 40 | var obj = EnvironmentObject() 41 | obj._wrappedValue = ViewBinderTestObject() 42 | return obj 43 | }() 44 | var body: View { Text("\(object.wrappedValue.change)") } 45 | } 46 | 47 | class ViewBinderMock: ViewBinder { 48 | var registered = false 49 | 50 | override func registerStateNotification(origin: Any) { 51 | super.registerStateNotification(origin: origin) 52 | registered = true 53 | } 54 | func testStateNotificationSubscribe() { 55 | XCTAssert(registered) 56 | registered = false 57 | } 58 | } 59 | 60 | // Tests 61 | 62 | func testViewBinderStateSubscribe() { 63 | let view = ViewBinderStateTestView() 64 | executeTestViewBinder(view) 65 | } 66 | func testViewBinderBindingSubscribe() { 67 | let stateView = ViewBinderStateTestView() 68 | let view = ViewBinderBindingTestView(state: stateView.$state) 69 | executeTestViewBinder(view) 70 | } 71 | func testViewBinderObservedObjectSubscribe() { 72 | let view = ViewBinderObservedObjTestView() 73 | executeTestViewBinder(view) 74 | } 75 | func testViewBinderStateObjectSubscribe() { 76 | let view = ViewBinderStateObjTestView() 77 | executeTestViewBinder(view) 78 | } 79 | func testViewBinderEnvironmentObjectSubscribe() { 80 | let view = ViewBinderEnvironmentObjTestView() 81 | executeTestViewBinder(view) 82 | } 83 | func executeTestViewBinder(_ view: View) { 84 | let binder = ViewBinderMock(view: view, rootController: nil, bodyLevel: 0, isInsideButton: false, overwriteTransaction: nil, parentScrollView: nil) 85 | EnvironmentHolder.currentBodyViewBinderStack.append(binder) 86 | _ = view.body 87 | EnvironmentHolder.currentBodyViewBinderStack.removeLast() 88 | binder.testStateNotificationSubscribe() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/Protocols/LazyStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LazyStack.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin | Kevs | TDD on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol LazyStack: Renderable, View { 11 | var viewContentBuilder: () -> View { get } 12 | var spacing: CGFloat { get } 13 | var noPropertiesStack: Stack { get } 14 | var scrollAxis: Axis { get } 15 | var stackAxis: NSLayoutConstraint.Axis { get } 16 | func updateStackAlignment(stack: SwiftUILazyStackView) 17 | } 18 | 19 | extension LazyStack { 20 | /// Insert latest updated view content. Should be called outside of actual 21 | /// create and update operations 22 | func insertRemainingViews(view: SwiftUILazyStackView) { 23 | view.insertViewsUntilVisibleArea() 24 | } 25 | 26 | func updateLoadedViews(view: SwiftUILazyStackView) { 27 | view.updateLazyStack( 28 | newViews: viewContentBuilder().totallyFlatSubViewsWithOptionalViewInfo, 29 | isEquallySpaced: noPropertiesStack.subviewIsEquallySpaced, 30 | setEqualDimension: noPropertiesStack.setSubviewEqualDimension) 31 | } 32 | } 33 | 34 | // MARK: - Renderable 35 | 36 | extension LazyStack { 37 | public func createView(context: Context) -> UIView { 38 | if let scrollView = context.parentScrollView, 39 | scrollView.axis == scrollAxis, 40 | scrollView.rootLazyStack == nil { 41 | let stackView = SwiftUILazyStackView().noAutoresizingMask() 42 | stackView.axis = stackAxis 43 | updateStackAlignment(stack: stackView) 44 | stackView.spacing = spacing 45 | stackView.lastContext = context 46 | stackView.lazyStackFlattenedContentViews = viewContentBuilder().totallyFlatSubViewsWithOptionalViewInfo 47 | stackView.lazyStackScrollView = scrollView 48 | 49 | context.postRenderOperationQueue.addOperation { 50 | let insertSubviews = { [weak stackView] in 51 | guard let stackView = stackView else { return } 52 | // Render operation initiated by UIKit layout 53 | // calls rather than AltSwiftUI render cycle. 54 | insertRemainingViews(view: stackView) 55 | } 56 | scrollView.executeOnNewLayout(insertSubviews) 57 | } 58 | scrollView.rootLazyStack = stackView 59 | 60 | if context.viewValues?.background != nil || context.viewValues?.border != nil { 61 | return BackgroundView(content: stackView).noAutoresizingMask() 62 | } else { 63 | return stackView 64 | } 65 | } else { 66 | return updatedStack.createView(context: context) 67 | } 68 | } 69 | 70 | public func updateView(_ view: UIView, context: Context) { 71 | var stackView = view 72 | if let bgView = view as? BackgroundView { 73 | stackView = bgView.content 74 | } 75 | 76 | if let stackView = stackView as? SwiftUILazyStackView, 77 | stackView.lazyStackScrollView != nil { 78 | stackView.lastContext = context 79 | updateLoadedViews(view: stackView) 80 | } else { 81 | let oldViewContent = (view.lastRenderableView?.view as? LazyStack)?.noPropertiesStack.viewContent 82 | updatedStack.updateView(stackView, context: context, oldViewContent: oldViewContent) 83 | } 84 | } 85 | 86 | var updatedStack: Stack { 87 | var stack = noPropertiesStack 88 | stack.viewStore = viewStore 89 | return stack 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Shapes/Circle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Circle.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Nodehi, Jabbar on 2020/09/08. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that represents a Circle shape. 12 | public struct Circle: Shape { 13 | public var viewStore = ViewValues() 14 | 15 | public var fillColor = Color.clear 16 | public var strokeBorderColor = Color.clear 17 | public var style = StrokeStyle() 18 | var trimStartFraction: CGFloat = 0 19 | var trimEndFraction: CGFloat = 1 20 | 21 | public var body: View { 22 | EmptyView() 23 | } 24 | 25 | public init() {} 26 | 27 | /// Trims the path of the shape by the specified fractions. 28 | public func trim(from startFraction: CGFloat = 0, to endFraction: CGFloat = 1) -> Self { 29 | var circle = self 30 | circle.trimStartFraction = startFraction 31 | circle.trimEndFraction = endFraction 32 | return circle 33 | } 34 | 35 | public func createView(context: Context) -> UIView { 36 | let view = AltShapeView().noAutoresizingMask() 37 | view.updateOnLayout = { [weak view] rect in 38 | guard let view = view else { return } 39 | updatePath(view: view, path: path(from: rect), animation: nil) 40 | } 41 | updateView(view, context: context.withoutTransaction) 42 | return view 43 | } 44 | 45 | public func updateView(_ view: UIView, context: Context) { 46 | guard let view = view as? AltShapeView else { return } 47 | let oldView = view.lastRenderableView?.view as? Circle 48 | 49 | let width = context.viewValues?.viewDimensions?.width ?? view.bounds.width 50 | let height = context.viewValues?.viewDimensions?.height ?? view.bounds.height 51 | let animation = context.transaction?.animation 52 | view.lastSizeFromViewUpdate = CGSize(width: width, height: height) 53 | 54 | if fillColor.rawColor != Color.clear.rawColor { 55 | updatePath( 56 | view: view, 57 | path: path( 58 | from: CGRect(x: 0, y: 0, width: width, height: height), 59 | startFraction: trimStartFraction, 60 | endFraction: trimEndFraction), 61 | animation: animation) 62 | } else { 63 | if context.viewValues?.viewDimensions != oldView?.viewStore.viewDimensions { 64 | updatePath(view: view, path: path(from: CGRect(x: 0, y: 0, width: width, height: height)), animation: animation) 65 | } 66 | performUpdate(layer: view.caShapeLayer, keyPath: "strokeStart", newValue: trimStartFraction, animation: animation, oldValue: oldView?.trimStartFraction) 67 | performUpdate(layer: view.caShapeLayer, keyPath: "strokeEnd", newValue: trimEndFraction, animation: animation, oldValue: oldView?.trimEndFraction) 68 | } 69 | updateShapeLayerValues(view: view, context: context) 70 | } 71 | 72 | private func path(from rect: CGRect, startFraction: CGFloat = 0, endFraction: CGFloat = 1) -> UIBezierPath { 73 | let minDimensions = min(rect.width, rect.height) 74 | let x = ((rect.width - minDimensions) / 2) + minDimensions / 2 75 | let y = ((rect.height - minDimensions) / 2) + minDimensions / 2 76 | let startAngle = CGFloat(-(Double.pi / 2)) + startFraction * CGFloat(Double.pi * 2) 77 | let endAngle = CGFloat(-(Double.pi / 2)) + endFraction * CGFloat(Double.pi * 2) 78 | 79 | return UIBezierPath(arcCenter: CGPoint(x: x, y: y), radius: minDimensions / 2, startAngle: startAngle, endAngle: endAngle, clockwise: true) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /docResources/Features.md: -------------------------------------------------------------------------------- 1 | # AltSwiftUI Features 2 | 3 | -
Navigation 4 |

5 | 6 | | Component | Supported | 7 | | --- | --- | 8 | | `NavigationLink` | :white_check_mark: | 9 | | `NavigationView` | :white_check_mark: | 10 | | Sheet | :white_check_mark: | 11 | | Master Detail | | 12 | | Sidebar | | 13 | 14 |

15 |
16 | -
Collections 17 |

18 | 19 | | Component | Supported | 20 | | --- | --- | 21 | | `ForEach` | :white_check_mark: | 22 | | `Group ` | :white_check_mark: | 23 | | `List ` | :white_check_mark: | 24 | | `ScrollView` | :white_check_mark: | 25 | | `Section` | :white_check_mark: | 26 | | `LazyGrid` | | 27 | 28 |

29 |
30 | -
Layout 31 |

32 | 33 | | Component | Supported | 34 | | --- | --- | 35 | | `GeometryReader` | :white_check_mark: | 36 | | `HStack` | :white_check_mark: | 37 | | `Spacer` | :white_check_mark: | 38 | | `VStack` | :white_check_mark: | 39 | | `ZStack` | :white_check_mark: | 40 | | `LazyVStack` | | 41 | | `LazyHStack` | | 42 | 43 |

44 |
45 | -
Controls and Display Views 46 |

47 | 48 | | Component | Supported | 49 | | --- | --- | 50 | | `Button` | :white_check_mark: | 51 | | `Color` | :white_check_mark: | 52 | | `DatePicker` | :white_check_mark: | 53 | | `Divider` | :white_check_mark: | 54 | | `Image` | :white_check_mark: | 55 | | `Picker` | :white_check_mark: | 56 | | `Slider` | :white_check_mark: | 57 | | `Stepper` | :white_check_mark: | 58 | | `TabView` | :white_check_mark: | 59 | | `Text` | :white_check_mark: | 60 | | `TextField` | :white_check_mark: | 61 | | `Toggle` | :white_check_mark: | 62 | | Gradients | | 63 | | `Link` | | 64 | | `Path` | | 65 | | `SecureField` | | 66 | | Shapes | :white_check_mark: | 67 | | `TextEditor` | | 68 | 69 |

70 |
71 | -
Overlays and Menus 72 |

73 | 74 | | Component | Supported | 75 | | --- | --- | 76 | | Action Sheet | :white_check_mark: | 77 | | Alert | :white_check_mark: | 78 | | AppStore Overlay | :white_check_mark: | 79 | | Context Menu | :white_check_mark: | 80 | | `Menu` | | 81 | 82 |

83 |
84 | -
State property wrappers 85 |

86 | 87 | | Component | Supported | 88 | | --- | --- | 89 | | `Binding` | :white_check_mark: | 90 | | `Environment` | :white_check_mark: | 91 | | `EnvironmentObject` | :white_check_mark: | 92 | | `ObservedObject` | :white_check_mark: | 93 | | `Published` | :white_check_mark: | 94 | | `State` | :white_check_mark: | 95 | | `StateObject` | :white_check_mark: | 96 | 97 |

98 |
99 | - Data Binding and Automatic View Updates 100 | - Gestures 101 | - Drag and Drop 102 | - Animations and transitions 103 | - UIKit representables 104 | - Xcode previews 105 | 106 | ## Roadmap 107 | 108 | - [x] Shapes 109 | - [ ] TextEditor 110 | - [ ] LazyGrid 111 | - [ ] EnvironmentObject sub hierarchies 112 | - [ ] Paths 113 | - [ ] Gradients 114 | - [x] SecureField 115 | - [ ] Menu 116 | - [ ] @main App initializer 117 | - [ ] @AppStorage 118 | - [ ] Extended Environment properties 119 | - [ ] Sidebar and Master Detail 120 | - [ ] Lazy stacks -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Controls/DatePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatePicker.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Chan, Chengwei on 2020/09/14. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that displays a wheel-style date picker that allows selecting 12 | /// a date in the range. 13 | public struct DatePicker: View { 14 | public var viewStore = ViewValues() 15 | 16 | var selection: Binding? 17 | var minimumDate = Date() 18 | var maximumDate: Date? 19 | var components: Components 20 | 21 | public enum DatePickerComponent { 22 | /// show date picker component 23 | case date 24 | 25 | /// show time picker component 26 | case hourAndMinute 27 | } 28 | 29 | public typealias Components = [DatePickerComponent] 30 | 31 | public var body: View { 32 | EmptyView() 33 | } 34 | 35 | /// Creates an instance that selects a date from within a range. 36 | /// 37 | /// - Parameters: 38 | /// - title: The title of view. We don't support it now. just keep it 39 | /// so we can have same format as swiftUI's DatePicker. 40 | /// - selection: The selected date within `range`. 41 | /// - range: The close range of the valid dates. 42 | /// - displayedComponents: The time components that composite the date 43 | /// picker, now only supports [date, hourAndMinute], [date], [hourAndMinute]. 44 | /// 45 | public init(_ title: String, selection: Binding, in range: ClosedRange, displayedComponents: Components) { 46 | self.selection = selection 47 | self.minimumDate = range.lowerBound 48 | self.maximumDate = range.upperBound 49 | self.components = displayedComponents 50 | } 51 | 52 | /// Creates an instance that selects a date from within a range. 53 | /// 54 | /// - Parameters: 55 | /// - title: The title of view. We don't support it now. just keep it 56 | /// so we can have same format as swiftUI's DatePicker. 57 | /// - selection: The selected date within `range`. 58 | /// - range: The partial range of the valid dates. 59 | /// - displayedComponents: The time components that composite the date 60 | /// picker, now only supports [date, hourAndMinute], [date], [hourAndMinute]. 61 | /// 62 | public init(_ title: String, selection: Binding, in range: PartialRangeFrom, displayedComponents: Components) { 63 | self.selection = selection 64 | self.minimumDate = range.lowerBound 65 | self.components = displayedComponents 66 | } 67 | } 68 | 69 | extension DatePicker: Renderable { 70 | public func createView(context: Context) -> UIView { 71 | let view = SwiftUIDatePicker().noAutoresizingMask() 72 | updateView(view, context: context) 73 | return view 74 | } 75 | public func updateView(_ view: UIView, context: Context) { 76 | guard let view = view as? SwiftUIDatePicker else { return } 77 | view.dateBinding = selection 78 | 79 | if #available(iOS 13.4, *) { 80 | view.preferredDatePickerStyle = .wheels 81 | } 82 | 83 | if components.contains(.date) && components.contains(.hourAndMinute) { 84 | view.datePickerMode = .dateAndTime 85 | } else if components.contains(.date) { 86 | view.datePickerMode = .date 87 | } else if components.contains(.hourAndMinute) { 88 | view.datePickerMode = .time 89 | } 90 | 91 | view.minimumDate = minimumDate 92 | view.date = selection?.wrappedValue ?? Date() 93 | if let maximumDate = maximumDate { 94 | view.maximumDate = maximumDate 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/VStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VStack.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/05. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// This view arranges subviews vertically. 12 | public struct VStack: View, Stack { 13 | public var viewStore = ViewValues() 14 | let viewContent: [View] 15 | let alignment: HorizontalAlignment 16 | let spacing: CGFloat? 17 | 18 | /// Creates an instance of a view that arranges subviews vertically. 19 | /// 20 | /// - Parameters: 21 | /// - alignment: The horizontal alignment guide for its children. Defaults to `center`. 22 | /// - spacing: The vertical distance between subviews. If not specified, 23 | /// the distance will be 0. 24 | /// - content: A view builder that creates the content of this stack. 25 | public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> View) { 26 | let contentView = content() 27 | viewContent = contentView.subViews 28 | self.alignment = alignment 29 | self.spacing = spacing 30 | viewStore.direction = .vertical 31 | } 32 | public var body: View { 33 | EmptyView() 34 | } 35 | } 36 | 37 | extension VStack: Renderable { 38 | public func updateView(_ view: UIView, context: Context) { 39 | let oldVStackViewContent = (view.lastRenderableView?.view as? VStack)?.viewContent 40 | updateView(view, context: context, oldViewContent: oldVStackViewContent) 41 | } 42 | 43 | public func createView(context: Context) -> UIView { 44 | let stack = SwiftUIStackView().noAutoresizingMask() 45 | stack.axis = .vertical 46 | setupView(stack, context: context) 47 | stack.addViews(viewContent, context: context, isEquallySpaced: subviewIsEquallySpaced, setEqualDimension: setSubviewEqualDimension) 48 | if context.viewValues?.background != nil || context.viewValues?.border != nil { 49 | return BackgroundView(content: stack).noAutoresizingMask() 50 | } else { 51 | return stack 52 | } 53 | } 54 | 55 | func updateView(_ view: UIView, context: Context, oldViewContent: [View]? = nil) { 56 | var stackView = view 57 | if let bgView = view as? BackgroundView { 58 | stackView = bgView.content 59 | } 60 | 61 | guard let concreteStackView = stackView as? SwiftUIStackView else { return } 62 | setupView(concreteStackView, context: context) 63 | 64 | if let oldViewContent = oldViewContent { 65 | concreteStackView.updateViews(viewContent, 66 | oldViews: oldViewContent, 67 | context: context, 68 | isEquallySpaced: subviewIsEquallySpaced, 69 | setEqualDimension: setSubviewEqualDimension) 70 | } 71 | } 72 | 73 | private func setupView(_ view: SwiftUIStackView, context: Context) { 74 | view.setStackAlignment(alignment: alignment) 75 | view.spacing = spacing ?? 0 76 | } 77 | 78 | var subviewIsEquallySpaced: (View) -> Bool { { view in 79 | if (view is Spacer || 80 | view.viewStore.viewDimensions?.maxHeight == CGFloat.limitForUI 81 | ) 82 | && 83 | (view.viewStore.viewDimensions?.height == nil) { 84 | return true 85 | } else { 86 | return false 87 | } 88 | } 89 | } 90 | 91 | var setSubviewEqualDimension: (UIView, UIView) -> Void { { firstView, secondView in 92 | firstView.heightAnchor.constraint(equalTo: secondView.heightAnchor).isActive = true 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /docResources/altswiftui.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/ForEach.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForEach.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/05. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A view that creates views based on data. This view itself has no visual 12 | /// representation. Adding a background to this view, for example, will have no effect. 13 | public struct ForEach: View { 14 | public var viewStore = ViewValues() 15 | var viewContent: [View] 16 | let data: Data 17 | let idKeyPath: KeyPath 18 | 19 | public var body: View { 20 | EmptyView() 21 | } 22 | 23 | /// Creates views based on uniquely identifiable data, by using a custom id. 24 | /// 25 | /// The `id` value of each data should be unique. 26 | /// 27 | /// - Parameters: 28 | /// - data: The identified data that the ``ForEach`` instance uses to 29 | /// create views dynamically. 30 | /// - id: A keypath for the value that uniquely identify each data. 31 | /// - content: The view builder that creates views dynamically. 32 | public init(_ data: Data, id: KeyPath, content: @escaping (Data.Element) -> Content) { 33 | viewContent = data.map { content($0) } 34 | self.data = data 35 | idKeyPath = id 36 | } 37 | } 38 | 39 | extension ForEach where ID == Data.Element.ID, Data.Element: Identifiable { 40 | 41 | /// Creates views based on uniquely identifiable data. 42 | /// 43 | /// The `id` value of each data should be unique. 44 | /// 45 | /// - Parameters: 46 | /// - data: The identified data that the ``ForEach`` instance uses to 47 | /// create views dynamically. 48 | /// - content: The view builder that creates views dynamically. 49 | public init(_ data: Data, content: @escaping (Data.Element) -> Content) { 50 | viewContent = data.map { content($0) } 51 | self.data = data 52 | idKeyPath = \Data.Element.id 53 | } 54 | } 55 | 56 | extension ForEach where Data == Range, ID == Int { 57 | 58 | /// Creates an instance that computes views on demand over a *constant* 59 | /// range. 60 | /// 61 | /// To compute views on demand over a dynamic range use 62 | /// `ForEach(_:id:content:)`. 63 | public init(_ data: Range, content: @escaping (Int) -> Content) { 64 | viewContent = data.map { content($0) } 65 | self.data = data 66 | idKeyPath = \Data.Element.self 67 | } 68 | } 69 | 70 | extension ForEach: ComparableViewGrouper { 71 | func iterateDiff(oldViewGroup: ComparableViewGrouper, startDisplayIndex: inout Int, iterate: (Int, DiffableViewSourceOperation) -> Void) { 72 | guard let oldViewGroup = oldViewGroup as? Self else { return } 73 | data.iterateDataDiff(oldData: oldViewGroup.data, id: id(for:), startIndex: startDisplayIndex) { currentDisplayIndex, collectionIndex, operation in 74 | startDisplayIndex = currentDisplayIndex + 1 75 | switch operation { 76 | case .insert: 77 | if case let .current(index) = collectionIndex { 78 | iterate(currentDisplayIndex, .insert(view: viewContent[index])) 79 | } 80 | case .delete: 81 | if case let .old(index) = collectionIndex { 82 | iterate(currentDisplayIndex, .delete(view: oldViewGroup.viewContent[index])) 83 | } 84 | case .update: 85 | if case let .current(index) = collectionIndex { 86 | iterate(currentDisplayIndex, .update(view: viewContent[index])) 87 | } 88 | } 89 | } 90 | } 91 | 92 | private func id(for item: Data.Element) -> ID { 93 | item[keyPath: idKeyPath] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Controls/Button.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that can be tapped by the user to trigger some action. 12 | public struct Button: View { 13 | public var viewStore = ViewValues() 14 | var labels: [View] 15 | var action: () -> Void 16 | 17 | /// Creates an instance that triggers an `action`. 18 | /// 19 | /// - Parameters: 20 | /// - action: The action to perform when the button is triggered. 21 | /// - label: The visual representation of the button 22 | public init(action: @escaping () -> Void, @ViewBuilder label: () -> View) { 23 | self.labels = label().subViews 24 | self.action = action 25 | } 26 | 27 | /// Performs the primary action. 28 | public func trigger() { 29 | action() 30 | } 31 | 32 | public var body: View { 33 | self 34 | } 35 | } 36 | 37 | extension Button { 38 | /// Creates an instance with a `Text` visual representation. 39 | /// 40 | /// - Parameters: 41 | /// - title: The title of the button. 42 | /// - action: The action to perform when the button is triggered. 43 | public init(_ title: String, action: @escaping () -> Void) { 44 | labels = [Text(title)] 45 | self.action = action 46 | } 47 | } 48 | 49 | extension Button: Renderable { 50 | public func updateView(_ view: UIView, context: Context) { 51 | guard let view = view as? SwiftUIButton, 52 | let lastView = view.lastRenderableView?.view as? Self, 53 | let firstLabel = labels.first, 54 | let firstOldLabel = lastView.labels.first else { return } 55 | let customContext = modifiedContext(context) 56 | 57 | context.viewOperationQueue.addOperation { 58 | [firstLabel].iterateFullViewDiff(oldList: [firstOldLabel]) { _, operation in 59 | switch operation { 60 | case .insert(let newView): 61 | if let newRenderView = newView.renderableView(parentContext: customContext, drainRenderQueue: false) { 62 | view.updateContentView(newRenderView) 63 | } 64 | case .delete: 65 | break 66 | case .update(let newView): 67 | newView.updateRender(uiView: view.contentView, parentContext: customContext, drainRenderQueue: false) 68 | } 69 | } 70 | } 71 | view.action = action 72 | } 73 | 74 | public func createView(context: Context) -> UIView { 75 | let customContext = modifiedContext(context) 76 | guard let contentView = labels.first?.renderableView(parentContext: customContext) else { return UIView() } 77 | 78 | let button = SwiftUIButton(contentView: contentView, action: action).noAutoresizingMask() 79 | if let buttonStyle = customContext.viewValues?.buttonStyle { 80 | button.animates = false 81 | let styledContentView = buttonStyle.makeBody(configuration: ButtonStyleConfiguration(label: labels[0], isPressed: false)) 82 | styledContentView.updateRender(uiView: contentView, parentContext: customContext) 83 | } 84 | 85 | return button 86 | } 87 | 88 | private func modifiedContext(_ context: Context) -> Context { 89 | var customContext = context 90 | customContext.isInsideButton = true 91 | 92 | // Set a default accentColor since SwiftUIButton subviews won't 93 | // take the button's tint color. 94 | if context.viewValues?.accentColor == nil { 95 | customContext.viewValues?.accentColor = Color.systemAccentColor.color 96 | } 97 | 98 | return customContext 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/ZStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZStack.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/05. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// This view arranges subviews one in front of the other, using the _z_ axis. 12 | public struct ZStack: View { 13 | public var viewStore = ViewValues() 14 | let viewContent: [View] 15 | let alignment: Alignment 16 | 17 | /// Creates an instance of a view that arranges subviews horizontally. 18 | /// 19 | /// - Parameters: 20 | /// - alignment: The alignment guide for its children. Defaults to `center`. 21 | /// - content: A view builder that creates the content of this stack. The 22 | /// last view will be the topmost view. 23 | public init(alignment: Alignment = .center, @ViewBuilder content: () -> View) { 24 | viewContent = content().subViews 25 | self.alignment = alignment 26 | } 27 | 28 | public var body: View { 29 | self 30 | } 31 | } 32 | 33 | extension ZStack: Renderable { 34 | public func createView(context: Context) -> UIView { 35 | let view = SwiftUIView().noAutoresizingMask() 36 | 37 | context.viewOperationQueue.addOperation { 38 | self.viewContent.iterateFullViewInsert { subView in 39 | if let renderView = subView.renderableView(parentContext: context, drainRenderQueue: false) { 40 | view.addSubview(renderView) 41 | LayoutSolver.solveLayout(parentView: view, contentView: renderView, content: subView, parentContext: context, alignment: self.alignment) 42 | } 43 | } 44 | } 45 | 46 | return view 47 | } 48 | 49 | public func updateView(_ view: UIView, context: Context) { 50 | if let oldZStack = view.lastRenderableView?.view as? Self { 51 | context.viewOperationQueue.addOperation { 52 | var indexSkip = 0 53 | self.viewContent.iterateFullViewDiff(oldList: oldZStack.viewContent) { i, operation in 54 | let index = i + indexSkip 55 | switch operation { 56 | case .insert(let suiView): 57 | if let subView = suiView.renderableView(parentContext: context, drainRenderQueue: false) { 58 | view.insertSubview(subView, at: index) 59 | LayoutSolver.solveLayout(parentView: view, contentView: subView, content: suiView, parentContext: context, alignment: self.alignment) 60 | suiView.performInsertTransition(view: subView, animation: context.transaction?.animation) {} 61 | } 62 | case .delete(let suiView): 63 | guard let subViewData = view.firstNonRemovingSubview(index: index) else { 64 | break 65 | } 66 | 67 | indexSkip += subViewData.skippedSubViews 68 | let subView = subViewData.uiView 69 | subView.isAnimatingRemoval = true 70 | if suiView.performRemovalTransition(view: subView, animation: context.transaction?.animation, completion: { 71 | subView.removeFromSuperview() 72 | }) { 73 | indexSkip -= 1 74 | } 75 | case .update(let suiView): 76 | guard let subViewData = view.firstNonRemovingSubview(index: index) else { 77 | break 78 | } 79 | 80 | indexSkip += subViewData.skippedSubViews 81 | let subView = subViewData.uiView 82 | suiView.updateRender(uiView: subView, parentContext: context, drainRenderQueue: false) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/ReadOnly/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that represents a color. 12 | /// 13 | /// By default, a `Color` that is directly rendered in a view hierarchy 14 | /// will expand both horizontally and vertically infinitely as much as 15 | /// its parent view allows it to. 16 | public struct Color: View { 17 | public var viewStore = ViewValues() 18 | 19 | /// Stores the original color held by this view 20 | var rawColor: UIColor 21 | 22 | /// Calculates the color of this view based on its properties, 23 | /// like opacity. 24 | var color: UIColor { 25 | if let opacity = viewStore.opacity { 26 | var alpha: CGFloat = 0 27 | rawColor.getRed(nil, green: nil, blue: nil, alpha: &alpha) 28 | return rawColor.withAlphaComponent(alpha * CGFloat(opacity)) 29 | } 30 | return rawColor 31 | } 32 | 33 | public init(white: Double, opacity: Double = 1) { 34 | self.init(UIColor(white: CGFloat(white), alpha: CGFloat(opacity))) 35 | } 36 | public init(red: Double, green: Double, blue: Double, opacity: Double = 1) { 37 | self.init(UIColor(red: CGFloat(red), green: CGFloat(green), blue: CGFloat(blue), alpha: CGFloat(opacity))) 38 | } 39 | public init(_ name: String, bundle: Bundle? = nil) { 40 | self.init(UIColor(named: name) ?? .black) 41 | } 42 | public init(_ uicolor: UIColor) { 43 | rawColor = uicolor 44 | } 45 | 46 | /// The opacity of the color. Applying opacity on top of a color 47 | /// that already contains an alpha channel, will multiply 48 | /// the color's alpha value. 49 | public func opacity(_ opacity: Double) -> Color { 50 | var view = self 51 | view.viewStore.opacity = opacity 52 | return view 53 | } 54 | 55 | public var body: View { 56 | self 57 | } 58 | } 59 | 60 | extension Color { 61 | public static let black = Color(.black) 62 | public static let white = Color(.white) 63 | public static let blue = Color(.systemBlue) 64 | public static let red = Color(.systemRed) 65 | public static let yellow = Color(.systemYellow) 66 | public static let green = Color(.systemGreen) 67 | public static let orange = Color(.systemOrange) 68 | public static let pink = Color(.systemPink) 69 | public static let purple = Color(.systemPurple) 70 | public static let gray = Color(.systemGray) 71 | public static let clear = Color(.clear) 72 | 73 | /// Color for primary content, ex: Texts 74 | public static var primary: Color { 75 | if #available(iOS 13.0, *) { 76 | return Color(.label) 77 | } else { 78 | return Color(UIColor(white: 0, alpha: 1)) 79 | } 80 | } 81 | 82 | /// Color for secondary content, ex: Texts 83 | public static var secondary: Color { 84 | if #available(iOS 13.0, *) { 85 | return Color(.secondaryLabel) 86 | } else { 87 | return Color(UIColor(red: 60/255, green: 60/255, blue: 67/255, alpha: 0.6)) 88 | } 89 | } 90 | 91 | static var systemAccentColor: Color { 92 | .blue 93 | } 94 | } 95 | 96 | extension Color: Renderable { 97 | public func createView(context: Context) -> UIView { 98 | let view = SwiftUIExpandView(expandWidth: true, expandHeight: true).noAutoresizingMask() 99 | updateView(view, context: context.withoutTransaction) 100 | return view 101 | } 102 | 103 | public func updateView(_ view: UIView, context: Context) { 104 | if let animation = context.transaction?.animation { 105 | animation.performAnimation { 106 | view.backgroundColor = rawColor 107 | } 108 | } else { 109 | view.backgroundColor = rawColor 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/CoreUI/UIHostingController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIHostingController.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// The UIKit ViewController that acts as parent of a AltSwiftUI View hierarchy. 12 | /// 13 | /// Initialize this controller with the view that you want to place at the 14 | /// top of the hierarchy. 15 | /// 16 | /// When using UIKit views, it's possible to interact with a AltSwiftUI views by 17 | /// passing a UIHostingController that contains a `View` hierarchy. 18 | /// 19 | open class UIHostingController: UINavigationController { 20 | /// Overrides the behavior of the current interactivePopGesture and enables/disables it accordingly. 21 | /// This property is `true` by default. 22 | /// - important: Not SwiftUI compatible. 23 | /// - note: Different to using UINavigationController's `interactivePopGesture?.isEnabled`, 24 | /// this property is able to turn on/off the gesture even if there is no existent `navigationBar` or if the `leftBarButtonItem` is set. 25 | public static var isInteractivePopGestureEnabled = true 26 | 27 | var sheetPresentation: SheetPresentation? 28 | 29 | /// Indicates if a UIViewController is currently being pushed onto this navigation controller 30 | private var duringPushAnimation = false 31 | 32 | public init(rootView: View, background: UIColor? = nil) { 33 | let controller = ScreenViewController(contentView: rootView, background: background) 34 | super.init(rootViewController: controller) 35 | setupNavigation() 36 | } 37 | 38 | override init(rootViewController: UIViewController) { 39 | super.init(rootViewController: rootViewController) 40 | setupNavigation() 41 | } 42 | 43 | override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { 44 | super.init(nibName: nil, bundle: nil) 45 | } 46 | 47 | public required init?(coder aDecoder: NSCoder) { 48 | fatalError("init(coder:) has not been implemented") 49 | } 50 | 51 | override public func pushViewController(_ viewController: UIViewController, animated: Bool) { 52 | duringPushAnimation = true 53 | super.pushViewController(viewController, animated: animated) 54 | } 55 | 56 | deinit { 57 | delegate = nil 58 | interactivePopGestureRecognizer?.delegate = nil 59 | } 60 | 61 | /** 62 | Set this property to use a custom implementation for the application's root 63 | UIHostingController when there is a TabView in the hierarchy. 64 | The TabView will cause the root controller to be recreated, so don't 65 | subclass a UIHostingController and replace it in the app's delegate. 66 | */ 67 | public static var customRootTabBarController = SwiftUITabBarController() 68 | private func setupNavigation() { 69 | delegate = self 70 | interactivePopGestureRecognizer?.delegate = self 71 | navigationBar.prefersLargeTitles = true 72 | } 73 | } 74 | 75 | extension UIHostingController: UINavigationControllerDelegate { 76 | public func navigationController( 77 | _ navigationController: UINavigationController, 78 | didShow viewController: UIViewController, 79 | animated: Bool) { 80 | 81 | (navigationController as? UIHostingController)?.duringPushAnimation = false 82 | } 83 | } 84 | 85 | extension UIHostingController: UIGestureRecognizerDelegate { 86 | public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 87 | guard gestureRecognizer == interactivePopGestureRecognizer else { return true } 88 | 89 | // Disable pop gesture when: 90 | // 1) the view controller has the isInteractivePopGesture disabled manually 91 | guard Self.isInteractivePopGestureEnabled else { return false } 92 | 93 | // 2) when the pop animation is in progress 94 | // 3) when user swipes quickly a couple of times and animations don't have time to be performed 95 | // 4) when there is only one view controller on the stack 96 | return viewControllers.count > 1 && !duringPushAnimation 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/CoreViews.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreViews.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2019/10/07. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Renderable Views 12 | 13 | /// An empty view with no content. 14 | public struct EmptyView: View { 15 | public var viewStore = ViewValues() 16 | public var body: View { 17 | self 18 | } 19 | public init() {} 20 | } 21 | 22 | extension EmptyView: Renderable { 23 | public func updateView(_ view: UIView, context: Context) { 24 | } 25 | 26 | public func createView(context: Context) -> UIView { 27 | SwiftUIEmptyView().noAutoresizingMask() 28 | } 29 | } 30 | 31 | /// A view that adds padding to another view. 32 | public struct PaddingView: View, Equatable { 33 | public static func == (lhs: PaddingView, rhs: PaddingView) -> Bool { 34 | if let lContent = lhs.contentView as? PaddingView, let rContent = rhs.contentView as? PaddingView { 35 | return lContent == rContent 36 | } else { 37 | return type(of: lhs.contentView) == type(of: rhs.contentView) 38 | } 39 | } 40 | 41 | public var viewStore = ViewValues() 42 | public var body: View { 43 | EmptyView() 44 | } 45 | var contentView: View 46 | var padding: CGFloat? 47 | var paddingInsets: EdgeInsets? 48 | } 49 | 50 | extension PaddingView: Renderable { 51 | public func createView(context: Context) -> UIView { 52 | let view = SwiftUIPaddingView().noAutoresizingMask() 53 | 54 | context.viewOperationQueue.addOperation { 55 | guard let renderedContentView = self.contentView.renderableView(parentContext: context, drainRenderQueue: false) else { return } 56 | view.content = renderedContentView 57 | self.setupView(view, context: context) 58 | } 59 | 60 | return view 61 | } 62 | 63 | public func updateView(_ view: UIView, context: Context) { 64 | guard let view = view as? SwiftUIPaddingView else { return } 65 | if let content = view.content { 66 | context.viewOperationQueue.addOperation { 67 | self.contentView.updateRender(uiView: content, parentContext: context, drainRenderQueue: false) 68 | self.setupView(view, context: context) 69 | } 70 | } 71 | } 72 | 73 | private func setupView(_ view: SwiftUIPaddingView, context: Context) { 74 | if let paddingInsets = paddingInsets { 75 | view.insets = UIEdgeInsets.withEdgeInsets(paddingInsets) 76 | } else if let padding = padding { 77 | view.insets = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding) 78 | } 79 | if context.transaction?.animation != nil { 80 | view.setNeedsLayout() 81 | } 82 | } 83 | } 84 | 85 | // MARK: - Builder Views 86 | 87 | public struct OptionalView: View { 88 | enum IfElseType: Equatable { 89 | /// Views inside 'if' statement 90 | case `if` 91 | /// Views inside 'else' statement 92 | case `else` 93 | /// View inside if statements. Used when views in multiple if/else levels 94 | /// are flattened. The `Int` value is used to uniquely identify each if/else block after 95 | /// flattening. 96 | case flattenedIf(Int) 97 | /// View inside else statements. Used when views in multiple if/else levels 98 | /// are flattened. The `Int` value is used to uniquely identify each if/else block after 99 | /// flattening. 100 | case flattenedElse(Int) 101 | } 102 | 103 | public var viewStore = ViewValues() 104 | public var body: View { 105 | EmptyView() 106 | } 107 | let content: [View]? 108 | var ifElseType: IfElseType? 109 | } 110 | 111 | public struct TupleView: View, ViewGrouper { 112 | public var viewStore = ViewValues() 113 | var viewContent: [View] 114 | 115 | public init(_ values: [View]) { 116 | viewContent = values 117 | } 118 | 119 | public var body: View { 120 | EmptyView() 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Controls/Menu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Menu.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Chan, Chengwei on 2021/02/25. 6 | // Copyright © 2021 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @available(iOS 14.0, *) 12 | /// A view that can be tapped by the user to open a menu. 13 | public struct Menu: View { 14 | public var viewStore = ViewValues() 15 | var label: View 16 | var viewContent: [View] 17 | 18 | /// Creates an instance that triggers an `action`. 19 | /// 20 | /// - Parameters: 21 | /// - content: A view builder that creates the content of menu options. 22 | /// Content can only support two specific types of view: 23 | /// 1. Button with Text inside 24 | /// 2. Menu 25 | /// - label: The visual representation of the menu button 26 | public init(@ViewBuilder content: () -> View, label: () -> View) { 27 | self.label = label() 28 | self.viewContent = content().subViews 29 | } 30 | 31 | public var body: View { 32 | self 33 | } 34 | } 35 | 36 | @available(iOS 14.0, *) 37 | extension Menu { 38 | /// Creates an instance with a `Text` visual representation. 39 | /// 40 | /// - Parameters: 41 | /// - title: The title of the button. 42 | /// - content: A view builder that creates the content of menu options. 43 | /// Content can only support two specific types of view: 44 | /// 1. Button with Text inside 45 | /// 2. Menu 46 | public init(_ title: String, @ViewBuilder content: () -> View) { 47 | label = Text(title) 48 | self.viewContent = content().subViews 49 | } 50 | } 51 | 52 | @available(iOS 14.0, *) 53 | extension Menu: Renderable { 54 | public func updateView(_ view: UIView, context: Context) { 55 | guard let view = view as? SwiftUIButton, 56 | let lastView = view.lastRenderableView?.view as? Self else { return } 57 | 58 | context.viewOperationQueue.addOperation { 59 | [label].iterateFullViewDiff(oldList: [lastView.label]) { _, operation in 60 | switch operation { 61 | case .insert(let newView): 62 | if let newRenderView = newView.renderableView(parentContext: context, drainRenderQueue: false) { 63 | view.updateContentView(newRenderView) 64 | } 65 | case .delete: 66 | break 67 | case .update(let newView): 68 | newView.updateRender(uiView: view.contentView, parentContext: context, drainRenderQueue: false) 69 | } 70 | } 71 | } 72 | view.menu = menu 73 | } 74 | 75 | public func createView(context: Context) -> UIView { 76 | let button = Button { 77 | 78 | } label: { () -> View in 79 | label 80 | } 81 | if let uiButton = button.createView(context: context) as? SwiftUIButton { 82 | uiButton.showsMenuAsPrimaryAction = true 83 | uiButton.menu = menu 84 | return uiButton 85 | } 86 | 87 | return UIView() 88 | } 89 | 90 | private var menu: UIMenu { 91 | UIMenu(title: "", image: nil, options: .displayInline, children: menuElements(viewContent: viewContent)) 92 | } 93 | 94 | private func menuElements(viewContent: [View]) -> [UIMenuElement] { 95 | var elements = [UIMenuElement]() 96 | viewContent.totallyFlatIterate { (view) in 97 | if let buttonView = view as? Button, 98 | let textView = buttonView.labels.first as? Text { 99 | let action = UIAction(title: textView.string, image: nil, handler: { _ in buttonView.action() }) 100 | elements.append(action) 101 | } else if let menuView = view as? Menu, let textView = menuView.label.subViews.first as? Text { 102 | let menu = UIMenu(title: textView.string, image: nil, children: menuElements(viewContent: menuView.viewContent)) 103 | elements.append(menu) 104 | } 105 | } 106 | return elements 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/ReadOnly/Image.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that renders an image. 12 | public struct Image: View { 13 | public enum ResizingMode { 14 | case stretch 15 | case tile 16 | 17 | var uiImageResizingMode: UIImage.ResizingMode { 18 | switch self { 19 | case .stretch: return .stretch 20 | case .tile: return .tile 21 | } 22 | } 23 | } 24 | 25 | public var viewStore = ViewValues() 26 | public var body: View { 27 | EmptyView() 28 | } 29 | private(set) var image: UIImage 30 | var isResizable = false 31 | var renderingMode: Image.TemplateRenderingMode? 32 | 33 | /// Initializes an `Image` with a `UIImage`. 34 | public init(uiImage image: UIImage) { 35 | self.image = image 36 | } 37 | 38 | /// Initializes an `Image` by looking up a image asset by name. 39 | /// Optionally specify a bundle to search in, if not, the app's main 40 | /// bundle will be used. 41 | public init(_ name: String, bundle: Bundle? = nil) { 42 | self.image = UIImage(named: name, in: bundle, compatibleWith: nil) ?? UIImage() 43 | } 44 | 45 | /// Specify if the image should dynamically stretch its contents. 46 | /// 47 | /// When resizable, the image will expand both horizontally and 48 | /// vertically infinitely as much as its parent allows it to. 49 | /// Also, when specifying a `frame`, the image contents will stretch 50 | /// to the dimensions of the specified `frame`. 51 | /// - Parameters: 52 | /// + capInsets: The values to use for the cap insets. 53 | /// + resizingMode: The mode with which the interior of the image is resized. 54 | /// - Note: Use `.scaledToFit()` and `.scaledToFill()` to modify how the aspect 55 | /// ratio of the image varies when stretching. 56 | public func resizable(capInsets: EdgeInsets = EdgeInsets(), resizingMode: Image.ResizingMode = .stretch) -> Self { 57 | var view = self 58 | view.isResizable = true 59 | view.image = view.image.resizableImage(withCapInsets: capInsets.uiEdgeInsets, resizingMode: resizingMode.uiImageResizingMode) 60 | return view.frame(maxWidth: .infinity, maxHeight: .infinity) 61 | } 62 | 63 | /// Sets the rendering mode of the image. 64 | public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> Image { 65 | var view = self 66 | view.renderingMode = renderingMode 67 | return view 68 | } 69 | } 70 | 71 | extension Image { 72 | public enum TemplateRenderingMode { 73 | case template, original 74 | } 75 | } 76 | 77 | extension Image: Renderable { 78 | public func createView(context: Context) -> UIView { 79 | let view = SwiftUIImageView(image: image).noAutoresizingMask() 80 | updateView(view, context: context) 81 | return view 82 | } 83 | public func updateView(_ view: UIView, context: Context) { 84 | guard let view = view as? SwiftUIImageView else { return } 85 | 86 | if let renderingMode = renderingMode { 87 | switch renderingMode { 88 | case .original: 89 | view.image = image.withRenderingMode(.alwaysOriginal) 90 | case .template: 91 | view.image = image.withRenderingMode(.alwaysTemplate) 92 | } 93 | } else { 94 | view.image = image 95 | } 96 | if !isResizable { 97 | view.contentMode = .center 98 | } else if let aspectRatioMode = context.viewValues?.aspectRatio?.contentMode { 99 | view.contentMode = aspectRatioMode.uiviewContentMode() 100 | } else { 101 | view.contentMode = .scaleToFill 102 | } 103 | } 104 | } 105 | 106 | extension ContentMode { 107 | func uiviewContentMode() -> UIView.ContentMode { 108 | switch self { 109 | case .fit: 110 | return .scaleAspectFit 111 | case .fill: 112 | return .scaleAspectFill 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Collections/HStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HStack.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/05. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// This view arranges subviews horizontally. 12 | public struct HStack: View, Stack { 13 | public var viewStore = ViewValues() 14 | let viewContent: [View] 15 | let alignment: VerticalAlignment 16 | let spacing: CGFloat 17 | 18 | /// Creates an instance of a view that arranges subviews horizontally. 19 | /// 20 | /// - Parameters: 21 | /// - alignment: The vertical alignment guide for its children. Defaults to `center`. 22 | /// - spacing: The horizontal distance between subviews. If not specified, 23 | /// the distance will use the default spacing specified by the framework. 24 | /// - content: A view builder that creates the content of this stack. 25 | public init(alignment: VerticalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> View) { 26 | let contentView = content() 27 | viewContent = contentView.mappedSubViews { subView in 28 | // Prevent UIStackView from creating custom 29 | // constraints that break children layout. 30 | if let text = subView as? Text, 31 | text.viewStore.lineLimit == nil, 32 | text.viewStore.viewDimensions?.width != nil { 33 | return VStack { subView } 34 | } else { 35 | return subView 36 | } 37 | } 38 | self.alignment = alignment 39 | self.spacing = spacing ?? SwiftUIConstants.defaultSpacing 40 | viewStore.direction = .horizontal 41 | } 42 | init(viewContent: [View]) { 43 | self.viewContent = viewContent 44 | alignment = .center 45 | spacing = SwiftUIConstants.defaultSpacing 46 | } 47 | public var body: View { 48 | EmptyView() 49 | } 50 | } 51 | 52 | extension HStack: Renderable { 53 | public func updateView(_ view: UIView, context: Context) { 54 | let oldHStackContent = (view.lastRenderableView?.view as? Self)?.viewContent 55 | updateView(view, context: context, oldViewContent: oldHStackContent) 56 | } 57 | 58 | public func createView(context: Context) -> UIView { 59 | let stack = SwiftUIStackView().noAutoresizingMask() 60 | setupView(stack, context: context) 61 | stack.addViews(viewContent, context: context, isEquallySpaced: subviewIsEquallySpaced, setEqualDimension: setSubviewEqualDimension) 62 | if context.viewValues?.background != nil || context.viewValues?.border != nil { 63 | return BackgroundView(content: stack).noAutoresizingMask() 64 | } else { 65 | return stack 66 | } 67 | } 68 | 69 | func updateView(_ view: UIView, context: Context, oldViewContent: [View]? = nil) { 70 | var stackView = view 71 | if let bgView = view as? BackgroundView { 72 | stackView = bgView.content 73 | } 74 | 75 | guard let concreteStackView = stackView as? SwiftUIStackView else { return } 76 | setupView(concreteStackView, context: context) 77 | 78 | if let oldViewContent = oldViewContent { 79 | concreteStackView.updateViews(viewContent, 80 | oldViews: oldViewContent, 81 | context: context, 82 | isEquallySpaced: subviewIsEquallySpaced, 83 | setEqualDimension: setSubviewEqualDimension) 84 | } 85 | } 86 | 87 | var subviewIsEquallySpaced: (View) -> Bool { { view in 88 | if (view is Spacer && 89 | view.viewStore.viewDimensions?.width == nil) 90 | || 91 | (view.viewStore.viewDimensions?.maxWidth == CGFloat.limitForUI) { 92 | return true 93 | } else { 94 | return false 95 | } 96 | } 97 | } 98 | 99 | var setSubviewEqualDimension: (UIView, UIView) -> Void { { firstView, secondView in 100 | firstView.widthAnchor.constraint(equalTo: secondView.widthAnchor).isActive = true 101 | } 102 | } 103 | 104 | private func setupView(_ view: SwiftUIStackView, context: Context) { 105 | view.setStackAlignment(alignment: alignment) 106 | view.spacing = spacing 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/ViewProperties/ViewPropertyTypes/ViewPropertyTransitionTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewPropertyTransitionTypes.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Public Types 12 | 13 | /// A type that represents a transition used by a view 14 | /// when being added or removed from a hierarchy. 15 | public struct AnyTransition { 16 | struct InternalTransition { 17 | var transform: CGAffineTransform? 18 | var opacity: CGFloat? 19 | var animation: Animation? 20 | var replaceTransform = false 21 | 22 | func combining(_ transition: InternalTransition) -> InternalTransition { 23 | var base = self 24 | if let newTransform = transition.transform { 25 | base.transform = transform?.concatenating(newTransform) ?? newTransform 26 | } 27 | if let newOpacity = transition.opacity { 28 | base.opacity = newOpacity 29 | } 30 | if let newAnimation = transition.animation { 31 | base.animation = newAnimation 32 | } 33 | return base 34 | } 35 | 36 | func performTransition(view: UIView) { 37 | if let opacity = opacity { 38 | view.alpha = opacity 39 | } 40 | if let transform = transform { 41 | setViewTransform(view: view, transform: transform) 42 | } 43 | } 44 | private func setViewTransform(view: UIView, transform: CGAffineTransform) { 45 | if replaceTransform { 46 | view.transform = transform 47 | } else { 48 | view.transform = view.transform.concatenating(transform) 49 | } 50 | } 51 | } 52 | var insertTransition: InternalTransition 53 | var removeTransition: InternalTransition? 54 | 55 | init(_ insertTransition: InternalTransition) { 56 | self.insertTransition = insertTransition 57 | } 58 | init(insert: InternalTransition, remove: InternalTransition) { 59 | self.insertTransition = insert 60 | self.removeTransition = remove 61 | } 62 | 63 | // MARK: Public methods 64 | 65 | /// Transitions from a specified offset 66 | public static func offset(_ offset: CGSize) -> AnyTransition { 67 | AnyTransition(InternalTransition(transform: CGAffineTransform(translationX: offset.width, y: offset.height))) 68 | } 69 | 70 | /// Transitions from a specified offset 71 | public static func offset(x: CGFloat = 0, y: CGFloat = 0) -> AnyTransition { 72 | AnyTransition(InternalTransition(transform: CGAffineTransform(translationX: x, y: y))) 73 | } 74 | 75 | /// Transitions by scaling from 0.01 76 | public static var scale: AnyTransition { 77 | AnyTransition(InternalTransition(transform: CGAffineTransform(scaleX: 0.01, y: 0.01))) 78 | } 79 | 80 | /// Transitions by scaling from the specified offset 81 | public static func scale(scale: CGFloat) -> AnyTransition { 82 | AnyTransition(InternalTransition(transform: CGAffineTransform(scaleX: scale, y: scale))) 83 | } 84 | 85 | /// A transition from transparent to opaque on insertion and opaque to 86 | /// transparent on removal. 87 | public static let opacity = AnyTransition(InternalTransition(opacity: 0)) 88 | 89 | /// A composite `Transition` that gives the result of two transitions both 90 | /// applied. 91 | public func combined(with other: AnyTransition) -> AnyTransition { 92 | var transition = self 93 | transition.insertTransition = transition.insertTransition.combining(other.insertTransition) 94 | if let newRemoveTransition = other.removeTransition { 95 | transition.removeTransition = transition.removeTransition?.combining(newRemoveTransition) ?? newRemoveTransition 96 | } else if let removeTransition = transition.removeTransition { 97 | transition.removeTransition = removeTransition.combining(other.insertTransition) 98 | } 99 | return transition 100 | } 101 | 102 | /// Attach an animation to this transition. 103 | public func animation(_ animation: Animation?) -> AnyTransition { 104 | var transition = self 105 | transition.insertTransition.animation = animation 106 | transition.removeTransition?.animation = animation 107 | return transition 108 | } 109 | 110 | /// A composite `Transition` that uses a different transition for 111 | /// insertion versus removal. 112 | public static func asymmetric(insertion: AnyTransition, removal: AnyTransition) -> AnyTransition { 113 | AnyTransition(insert: insertion.insertTransition, remove: removal.insertTransition) 114 | } 115 | 116 | /// A transition that has no change in state. 117 | public static var identity: AnyTransition { 118 | AnyTransition(InternalTransition()) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/ViewProperties/ViewPropertyTypes/ViewPropertyViewTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewPropertyViewTypes.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import StoreKit 11 | 12 | // MARK: - Public Types 13 | 14 | /// Type that represents a system alert. Use it to customize the 15 | /// texts and actions that will be shown in the alert. 16 | public struct Alert { 17 | let title: String 18 | let message: String? 19 | let primaryButton: Alert.Button? 20 | let secondaryButton: Alert.Button? 21 | var alertIsPresented: Binding? 22 | /// Whether the alert should be displayed from the foremost view or not. This is not a native SwiftUI feature 23 | var displayOnForegroundView = false 24 | 25 | /// Creates an alert with one button. 26 | public init(title: Text, message: Text? = nil, dismissButton: Alert.Button? = nil) { 27 | self.title = title.string 28 | self.message = message?.string 29 | primaryButton = dismissButton 30 | secondaryButton = nil 31 | } 32 | 33 | /// Creates an alert with two buttons. 34 | /// 35 | /// - Note: the system determines the visual ordering of the buttons. 36 | public init(title: Text, message: Text? = nil, primaryButton: Alert.Button, secondaryButton: Alert.Button) { 37 | self.title = title.string 38 | self.message = message?.string 39 | self.primaryButton = primaryButton 40 | self.secondaryButton = secondaryButton 41 | } 42 | 43 | /// A button representing an operation of an alert presentation. 44 | public struct Button { 45 | enum Style { 46 | case `default`, cancel, destructive 47 | } 48 | 49 | let text: String 50 | let action: (() -> Void)? 51 | let style: Style 52 | 53 | /// Creates an `Alert.Button` with the default style. 54 | public static func `default`(_ label: Text, action: (() -> Void)? = {}) -> Alert.Button { 55 | Button(text: label.string, action: action, style: .default) 56 | } 57 | 58 | /// Creates an `Alert.Button` that indicates cancellation of some 59 | /// operation. 60 | public static func cancel(_ label: Text, action: (() -> Void)? = {}) -> Alert.Button { 61 | Button(text: label.string, action: action, style: .cancel) 62 | } 63 | 64 | /// Creates an `Alert.Button` with a style indicating destruction of 65 | /// some data. 66 | public static func destructive(_ label: Text, action: (() -> Void)? = {}) -> Alert.Button { 67 | Button(text: label.string, action: action, style: .destructive) 68 | } 69 | } 70 | } 71 | 72 | /// Type that represents a system action sheet. Use it to customize the 73 | /// texts and actions that will be shown in the action sheet. 74 | @available(OSX, unavailable) 75 | public struct ActionSheet { 76 | let title: String? 77 | let message: String? 78 | let buttons: [Button] 79 | var actionSheetIsPresented: Binding? 80 | /// Whether the alert should be displayed from the foremost view or not. This is not a native SwiftUI feature 81 | var displayOnForegroundView = false 82 | 83 | /// Creates an action sheet with the provided buttons. 84 | /// 85 | /// Send nil in the title to hide the title space in the 86 | /// action sheet. This behavior is not compatible with SwiftUI. 87 | public init(title: Text? = nil, message: Text? = nil, buttons: [ActionSheet.Button]) { 88 | self.title = title?.string 89 | self.message = message?.string 90 | self.buttons = buttons 91 | } 92 | 93 | /// A button representing an operation of an action sheet presentation. 94 | public typealias Button = Alert.Button 95 | } 96 | 97 | /// A container whose view content children will be presented as a menu items 98 | /// in a contextual menu after completion of the standard system gesture. 99 | /// 100 | /// The controls contained in a `ContextMenu` should be related to the context 101 | /// they are being shown from. 102 | /// 103 | /// - SeeAlso: `View.contextMenu`, which attaches a `ContextMenu` to a `View`. 104 | @available(tvOS, unavailable) 105 | public struct ContextMenu { 106 | let items: [View] 107 | 108 | /// __Important__: Only Buttons with `Image` or `Text` are allowed as items. 109 | /// The following 3 view combinations are allowed for building a contextual menu: 110 | /// 111 | /// ContextMenu { 112 | /// // First combination 113 | /// Button(Text("Add")) {} 114 | /// // Second combination 115 | /// Button(action: {}) { 116 | /// Image() 117 | /// } 118 | /// // Third combination 119 | /// Button(action: {}) { 120 | /// Text("Add") 121 | /// Image() 122 | /// } 123 | /// } 124 | public init(@ViewBuilder menuItems: () -> View) { 125 | items = menuItems().subViews 126 | } 127 | } 128 | 129 | // MARK: - Internal Types 130 | 131 | @available(iOS 14.0, *) 132 | @available(macCatalyst, unavailable) 133 | @available(OSX, unavailable) 134 | @available(tvOS, unavailable) 135 | @available(watchOS, unavailable) 136 | struct SKOverlayPresentation { 137 | let isPresented: Binding 138 | let configuration: () -> SKOverlay.Configuration 139 | } 140 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/CoreUI/ViewBinder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewBinder.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// This class holds and describes the relationship between a 12 | /// `View` and a `UIView`. A `View` is associated to a single `UIView`, but 13 | /// a `UIView` may be associated to multiple views differentiating them by their 14 | /// `bodyLevel`. 15 | class ViewBinder { 16 | enum StateNotification { 17 | static let name = Notification.Name(rawValue: "AltSwiftUI.Notification.State") 18 | static let transactionKey = "Transaction" 19 | } 20 | struct OverwriteTransaction { 21 | var transaction: Transaction 22 | weak var parent: UIView? 23 | } 24 | 25 | var view: View 26 | weak var uiView: UIView? 27 | weak var rootController: ScreenViewController? 28 | weak var overwriteRootController: UIViewController? 29 | var isQueuingTransactionUpdate = [Transaction: Bool]() 30 | var isQueuingStandardUpdate = false 31 | var isInsideButton: Bool 32 | var overwriteTransaction: OverwriteTransaction? 33 | weak var parentScrollView: SwiftUIScrollView? 34 | 35 | /// The body level describes how many parent views the current view 36 | /// has to traverse to reach the topmost View in the hierarchy associated 37 | /// to the same `UIView`. 38 | var bodyLevel: Int 39 | 40 | init(view: View, rootController: ScreenViewController?, bodyLevel: Int, isInsideButton: Bool, overwriteTransaction: OverwriteTransaction?, parentScrollView: SwiftUIScrollView?) { 41 | self.view = view 42 | self.rootController = rootController 43 | self.bodyLevel = bodyLevel 44 | self.isInsideButton = isInsideButton 45 | self.overwriteTransaction = overwriteTransaction 46 | self.parentScrollView = parentScrollView 47 | } 48 | 49 | func registerStateNotification(origin: Any) { 50 | NotificationCenter.default.removeObserver(self, name: Self.StateNotification.name, object: origin) 51 | NotificationCenter.default.addObserver(self, selector: #selector(handleStateNotification(notification:)), name: Self.StateNotification.name, object: origin) 52 | } 53 | 54 | // MARK: - Private methods 55 | 56 | private func updateView(transaction: Transaction?) { 57 | if let subView = uiView { 58 | assert(rootController?.lazyLayoutConstraints.isEmpty ?? true, "State changed while the body is being executed") 59 | if transaction?.animation != nil { 60 | rootController?.view.layoutIfNeeded() 61 | } else if overwriteTransaction?.transaction.animation != nil { 62 | overwriteTransaction?.parent?.layoutIfNeeded() 63 | } 64 | 65 | let postRenderQueue = ViewOperationQueue() 66 | view.updateRender( 67 | uiView: subView, 68 | parentContext: Context( 69 | rootController: rootController, 70 | overwriteRootController: overwriteRootController, 71 | transaction: overwriteTransaction?.transaction ?? transaction, 72 | postRenderOperationQueue: postRenderQueue, 73 | parentScrollView: parentScrollView, 74 | isInsideButton: isInsideButton), 75 | bodyLevel: bodyLevel) 76 | rootController?.executeLazyConstraints() 77 | rootController?.executeInsertAppearHandlers() 78 | 79 | overwriteTransaction?.transaction.animation?.performAnimation({ [weak self] in 80 | self?.overwriteTransaction?.parent?.layoutIfNeeded() 81 | }) 82 | transaction?.animation?.performAnimation({ [weak self] in 83 | self?.rootController?.view.layoutIfNeeded() 84 | }) 85 | 86 | postRenderQueue.drainRecursively() 87 | } 88 | } 89 | @objc private func handleStateNotification(notification: Notification) { 90 | if notification.name == Self.StateNotification.name { 91 | let transaction = notification.userInfo?[Self.StateNotification.transactionKey] as? Transaction 92 | // TODO: Improves view update performance, 93 | // but causes small delay. Need to find better way 94 | // to queue without delay, before render happens. 95 | // queueRenderUpdate(transaction: transaction) 96 | updateView(transaction: transaction) 97 | } 98 | } 99 | private func queueRenderUpdate(transaction: Transaction?) { 100 | if let transaction = transaction { 101 | if isQueuingTransactionUpdate[transaction] == true { 102 | return 103 | } else { 104 | isQueuingTransactionUpdate[transaction] = true 105 | DispatchQueue.main.async { [weak self] in 106 | self?.updateView(transaction: transaction) 107 | self?.isQueuingTransactionUpdate[transaction] = false 108 | } 109 | } 110 | } else { 111 | if isQueuingStandardUpdate { 112 | return 113 | } else { 114 | isQueuingStandardUpdate = true 115 | DispatchQueue.main.async { [weak self] in 116 | self?.updateView(transaction: transaction) 117 | self?.isQueuingStandardUpdate = false 118 | } 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/CoreUI/LayoutSolver.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutSolver.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// In charge of setting the correct layout constraints for a `View` configuration. 12 | enum LayoutSolver { 13 | // swiftlint:disable:next function_body_length 14 | static func solveLayout(parentView: UIView, contentView: UIView, content: View, parentContext: Context, expand: Bool = false, alignment: Alignment = .center) { 15 | var safeTop = true 16 | var safeLeft = true 17 | var safeRight = true 18 | var safeBottom = true 19 | let content = content.firstRenderableView(parentContext: parentContext) 20 | let edges = content.viewStore.edgesIgnoringSafeArea 21 | let rootView = edges != nil 22 | if let ignoringEdges = edges { 23 | if ignoringEdges.contains(.top) { 24 | safeTop = false 25 | } 26 | if ignoringEdges.contains(.leading) { 27 | safeLeft = false 28 | } 29 | if ignoringEdges.contains(.bottom) { 30 | safeBottom = false 31 | } 32 | if ignoringEdges.contains(.trailing) { 33 | safeRight = false 34 | } 35 | } 36 | 37 | let originalParentView = parentView 38 | var parentView = parentView 39 | var lazy = false 40 | if let contextController = parentContext.rootController, rootView { 41 | parentView = contextController.view 42 | lazy = true 43 | } 44 | var constraints = [NSLayoutConstraint]() 45 | 46 | if expand { 47 | constraints = contentView.edgesAnchorEqualTo(view: parentView, safeLeft: safeLeft, safeTop: safeTop, safeRight: safeRight, safeBottom: safeBottom) 48 | } else { 49 | switch alignment { 50 | case .center: 51 | if safeLeft && safeRight { 52 | constraints.append(contentsOf: [contentView.centerXAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.centerXAnchor)]) 53 | } else if !rootView { 54 | constraints.append(contentsOf: [contentView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor)]) 55 | } 56 | if safeTop && safeBottom { 57 | constraints.append(contentsOf: [contentView.centerYAnchor.constraint(equalTo: parentView.safeAreaLayoutGuide.centerYAnchor)]) 58 | } else if !rootView { 59 | constraints.append(contentsOf: [contentView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor)]) 60 | } 61 | case .leading: 62 | constraints = [contentView.leftAnchorEquals(parentView, safe: safeLeft), 63 | contentView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor)] 64 | case .trailing: 65 | constraints = [contentView.rightAnchorEquals(parentView, safe: safeRight), 66 | contentView.centerYAnchor.constraint(equalTo: parentView.centerYAnchor)] 67 | case .top: 68 | constraints = [contentView.topAnchorEquals(parentView, safe: safeTop), 69 | contentView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor)] 70 | case .bottom: 71 | constraints = [contentView.bottomAnchorEquals(parentView, safe: safeBottom), 72 | contentView.centerXAnchor.constraint(equalTo: parentView.centerXAnchor)] 73 | case .bottomLeading: 74 | constraints = [contentView.bottomAnchorEquals(parentView, safe: safeBottom), 75 | contentView.leftAnchorEquals(parentView, safe: safeLeft)] 76 | case .bottomTrailing: 77 | constraints = [contentView.bottomAnchorEquals(parentView, safe: safeBottom), 78 | contentView.rightAnchorEquals(parentView, safe: safeRight)] 79 | case .topLeading: 80 | constraints = [contentView.topAnchorEquals(parentView, safe: safeTop), 81 | contentView.leftAnchorEquals(parentView, safe: safeLeft)] 82 | case .topTrailing: 83 | constraints = [contentView.topAnchorEquals(parentView, safe: safeTop), 84 | contentView.rightAnchorEquals(parentView, safe: safeRight)] 85 | default: break 86 | } 87 | 88 | if rootView { 89 | if !safeTop { 90 | constraints.append(contentView.topAnchor.constraint(equalTo: parentView.topAnchor)) 91 | } 92 | if !safeBottom { 93 | constraints.append(contentView.bottomAnchor.constraint(equalTo: parentView.bottomAnchor)) 94 | } 95 | if !safeLeft { 96 | constraints.append(contentView.leftAnchor.constraint(equalTo: parentView.leftAnchor)) 97 | } 98 | if !safeRight { 99 | constraints.append(contentView.rightAnchor.constraint(equalTo: parentView.rightAnchor)) 100 | } 101 | } 102 | constraints.append(contentsOf: contentView.edgesGreaterOrEqualTo(view: parentView, safeLeft: safeLeft, safeTop: safeTop, safeRight: safeRight, safeBottom: safeBottom)) 103 | if rootView && parentView != originalParentView { 104 | constraints.append(contentsOf: contentView.edgesGreaterOrEqualTo(view: originalParentView, safeLeft: safeLeft, safeTop: safeTop, safeRight: safeRight, safeBottom: safeBottom, priority: .defaultHigh)) 105 | } 106 | } 107 | 108 | if !lazy { 109 | constraints.activate() 110 | } else if let controller = parentContext.rootController { 111 | controller.lazyLayoutConstraints.append(contentsOf: constraints) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/ViewProperties/ViewPropertyTypes/ViewPropertyGestureTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewPropertyGestureTypes.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/07/29. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - Public Types 12 | 13 | /// This protocol represents a stream of actions that will be performed 14 | /// based on the implemented gesture. 15 | public protocol Gesture { 16 | associatedtype Value 17 | associatedtype Body: Gesture 18 | 19 | /// Storage for onChanged events 20 | var onChanged: ((Self.Value) -> Void)? { get set } 21 | 22 | /// Storage for onChanged events 23 | var onEnded: ((Self.Value) -> Void)? { get set } 24 | 25 | /// Returns the concrete gesture that this gesture represents 26 | var body: Self.Body { get } 27 | } 28 | 29 | extension Gesture { 30 | /// Event that fires when the value of an active gesture changes 31 | public func onChanged(_ action: @escaping (Self.Value) -> Void) -> Self { 32 | var gesture = self 33 | gesture.onChanged = action 34 | return gesture 35 | } 36 | 37 | /// Event that fires when an active gesture ends 38 | public func onEnded(_ action: @escaping (Self.Value) -> Void) -> Self { 39 | var gesture = self 40 | gesture.onEnded = action 41 | return gesture 42 | } 43 | 44 | internal func firstExecutableGesture(level: Int = 0) -> ExecutableGesture? { 45 | if level == 2 { 46 | // Gestures that don't contain executable gesture won't be 47 | // counted as valid executable gestures 48 | return nil 49 | } 50 | 51 | if let executableGesture = self as? ExecutableGesture { 52 | return executableGesture 53 | } else { 54 | return body.firstExecutableGesture(level: level + 1) 55 | } 56 | } 57 | } 58 | 59 | /// This type will handle events of a user's tap gesture. 60 | public struct TapGesture: Gesture, ExecutableGesture { 61 | var priority: GesturePriority = .default 62 | public var onChanged: ((Self.Value) -> Void)? 63 | public var onEnded: ((Self.Value) -> Void)? 64 | 65 | public init() { 66 | self.onChanged = nil 67 | self.onEnded = nil 68 | } 69 | 70 | public var body: TapGesture { 71 | TapGesture() 72 | } 73 | 74 | // MARK: ExecutableGesture 75 | 76 | func recognizer(target: Any?, action: Selector?) -> UIGestureRecognizer { 77 | let gesture = UITapGestureRecognizer(target: target, action: action) 78 | gesture.cancelsTouchesInView = false 79 | return gesture 80 | } 81 | func processGesture(gestureRecognizer: UIGestureRecognizer, holder: GestureHolder) { 82 | onEnded?(()) 83 | } 84 | 85 | public typealias Value = Void 86 | } 87 | 88 | /// This type will handle events of a user's drag gesture. 89 | public struct DragGesture: Gesture, ExecutableGesture { 90 | public struct Value: Equatable { 91 | 92 | /// The location of the current event. 93 | public var location: CGPoint 94 | 95 | /// The location of the first event. 96 | public var startLocation: CGPoint 97 | 98 | /// The total translation from the first event to the current 99 | /// event. Equivalent to `location.{x,y} - 100 | /// startLocation.{x,y}`. 101 | public var translation: CGSize 102 | } 103 | 104 | var priority: GesturePriority = .default 105 | public var onChanged: ((Self.Value) -> Void)? 106 | public var onEnded: ((Self.Value) -> Void)? 107 | 108 | public init() { 109 | self.onChanged = nil 110 | self.onEnded = nil 111 | } 112 | 113 | public var body: DragGesture { 114 | DragGesture() 115 | } 116 | 117 | // MARK: ExecutableGesture 118 | 119 | func recognizer(target: Any?, action: Selector?) -> UIGestureRecognizer { 120 | UIPanGestureRecognizer(target: target, action: action) 121 | } 122 | 123 | func processGesture(gestureRecognizer: UIGestureRecognizer, holder: GestureHolder) { 124 | guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { return } 125 | let translation = panGesture.translation(in: panGesture.view) 126 | let location = panGesture.location(in: panGesture.view) 127 | let value = Value(location: location, startLocation: holder.firstLocation, translation: CGSize(width: translation.x, height: translation.y)) 128 | switch panGesture.state { 129 | case .began: 130 | holder.firstLocation = location 131 | case .changed: 132 | withHighPerformance { 133 | self.onChanged?(value) 134 | } 135 | case .ended: 136 | onEnded?(value) 137 | case .cancelled: 138 | onEnded?(value) 139 | default: break 140 | } 141 | } 142 | } 143 | 144 | // MARK: - Internal Types 145 | 146 | enum GesturePriority { 147 | case `default`, high, simultaneous 148 | } 149 | 150 | protocol ExecutableGesture { 151 | var priority: GesturePriority { get set } 152 | func recognizer(target: Any?, action: Selector?) -> UIGestureRecognizer 153 | func processGesture(gestureRecognizer: UIGestureRecognizer, holder: GestureHolder) 154 | } 155 | 156 | class GestureHolder: NSObject { 157 | var gesture: ExecutableGesture 158 | var isSimultaneous = false 159 | var firstLocation: CGPoint = .zero 160 | 161 | init(gesture: ExecutableGesture) { 162 | self.gesture = gesture 163 | } 164 | 165 | @objc func processGesture(gestureRecognizer: UIGestureRecognizer) { 166 | gesture.processGesture(gestureRecognizer: gestureRecognizer, holder: self) 167 | } 168 | } 169 | 170 | extension GestureHolder: UIGestureRecognizerDelegate { 171 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 172 | isSimultaneous 173 | } 174 | } 175 | 176 | class GestureHolders: NSObject { 177 | var gestures: [GestureHolder] 178 | init(gestures: [GestureHolder]) { 179 | self.gestures = gestures 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/UIKitCompat/UIKitCompat.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIKitCompat.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2019/10/07. 6 | // Copyright © 2019 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | // MARK: - UIViewControllerRepresentable 12 | 13 | /// The context used in a `UIViewControllerRepresentable` type. 14 | public struct UIViewControllerRepresentableContext where Representable: UIViewControllerRepresentable { 15 | 16 | /// The view's associated coordinator. 17 | public let coordinator: Representable.Coordinator 18 | 19 | /// The current `Transaction`. 20 | public var transaction: Transaction 21 | 22 | /// The current `Environment`. 23 | public var environment: EnvironmentValues 24 | } 25 | 26 | /// Use this protocol to create a custom `View` that represents a `UIViewController`. 27 | public protocol UIViewControllerRepresentable: View, Renderable { 28 | associatedtype UIViewControllerType: UIViewController 29 | typealias UIContext = UIViewControllerRepresentableContext 30 | associatedtype Coordinator = Void 31 | 32 | /// Creates a `UIViewController` instance to be presented. 33 | func makeUIViewController(context: UIContext) -> UIViewControllerType 34 | 35 | /// Updates the presented `UIViewController` (and coordinator) to the latest 36 | /// configuration. 37 | func updateUIViewController(_ uiViewController: UIViewControllerType, context: UIContext) 38 | 39 | /// Creates a `Coordinator` instance to coordinate with the 40 | /// `UIViewController`. 41 | /// 42 | /// `Coordinator` can be accessed via `Context`. 43 | func makeCoordinator() -> Coordinator 44 | } 45 | 46 | extension UIViewControllerRepresentable where Coordinator == Void { 47 | public func makeCoordinator() {} 48 | } 49 | 50 | extension UIViewControllerRepresentable { 51 | public var body: View { 52 | EmptyView() 53 | } 54 | public func createView(context: Context) -> UIView { 55 | guard let rootController = context.rootController else { return UIView() } 56 | 57 | let coordinator = makeCoordinator() 58 | let context = UIContext(coordinator: coordinator, transaction: context.transaction ?? Transaction(), environment: EnvironmentValues(rootController: rootController)) 59 | let controller = makeUIViewController(context: context) 60 | rootController.addChild(controller) 61 | controller.view.uiViewRepresentableCoordinator = coordinator as AnyObject 62 | updateUIViewController(controller, context: context) 63 | return controller.view.noAutoresizingMask() 64 | } 65 | public func updateView(_ view: UIView, context: Context) { 66 | guard let rootController = context.rootController else { return } 67 | 68 | if let controller = (rootController.children.first { $0.view == view }) as? UIViewControllerType, 69 | let coordinator = controller.view.uiViewRepresentableCoordinator as? Coordinator { 70 | let context = UIContext(coordinator: coordinator, transaction: context.transaction ?? Transaction(), environment: EnvironmentValues(rootController: rootController)) 71 | updateUIViewController(controller, context: context) 72 | } 73 | } 74 | } 75 | 76 | // MARK: - UIViewRepresentable 77 | 78 | /// The context used in a `UIViewRepresentable` type. 79 | public struct UIViewRepresentableContext where Representable: UIViewRepresentable { 80 | 81 | /// The view's associated coordinator. 82 | public let coordinator: Representable.Coordinator 83 | 84 | /// The current `Transaction`. 85 | public var transaction: Transaction 86 | 87 | /// The current `Environment`. 88 | public var environment: EnvironmentValues 89 | } 90 | 91 | /// Use this protocol to create a custom `View` that represents a `UIView`. 92 | public protocol UIViewRepresentable: View, Renderable { 93 | associatedtype UIViewType: UIView 94 | typealias UIContext = UIViewRepresentableContext 95 | associatedtype Coordinator = Void 96 | 97 | /// Creates a `UIView` instance to be presented. 98 | func makeUIView(context: UIContext) -> UIViewType 99 | 100 | /// Updates the presented `UIView` (and coordinator) to the latest 101 | /// configuration. 102 | func updateUIView(_ uiView: UIViewType, context: UIContext) 103 | 104 | /// Creates a `Coordinator` instance to coordinate with the 105 | /// `UIView`. 106 | /// 107 | /// `Coordinator` can be accessed via `Context`. 108 | func makeCoordinator() -> Coordinator 109 | } 110 | 111 | extension UIViewRepresentable where Coordinator == Void { 112 | public func makeCoordinator() {} 113 | } 114 | 115 | extension UIViewRepresentable { 116 | public var body: View { 117 | EmptyView() 118 | } 119 | public func createView(context: Context) -> UIView { 120 | let coordinator = makeCoordinator() 121 | let context = UIContext(coordinator: coordinator, transaction: context.transaction ?? Transaction(), environment: EnvironmentValues(rootController: context.rootController)) 122 | let uiView = makeUIView(context: context).noAutoresizingMask() 123 | uiView.uiViewRepresentableCoordinator = coordinator as AnyObject 124 | updateUIView(uiView, context: context) 125 | return uiView 126 | } 127 | public func updateView(_ view: UIView, context: Context) { 128 | if let view = view as? UIViewType, let coordinator = view.uiViewRepresentableCoordinator as? Coordinator { 129 | let context = UIContext(coordinator: coordinator, transaction: context.transaction ?? Transaction(), environment: EnvironmentValues(rootController: context.rootController)) 130 | updateUIView(view, context: context) 131 | } 132 | } 133 | } 134 | 135 | extension UIView { 136 | static var uiViewRepresentableCoordinatorAssociatedKey = "UIViewRepresentableCoordinatorAssociatedKey" 137 | var uiViewRepresentableCoordinator: AnyObject? { 138 | get { 139 | objc_getAssociatedObject(self, &Self.uiViewRepresentableCoordinatorAssociatedKey) as AnyObject 140 | } 141 | set { 142 | objc_setAssociatedObject(self, &Self.uiViewRepresentableCoordinatorAssociatedKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/AltSwiftUI/Source/Views/Controls/TextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextField.swift 3 | // AltSwiftUI 4 | // 5 | // Created by Wong, Kevin a on 2020/08/06. 6 | // Copyright © 2020 Rakuten Travel. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// A view that allows text input in one line. 12 | public struct TextField: View { 13 | public var viewStore = ViewValues() 14 | let title: String 15 | let onCommit: () -> Void 16 | let onEditingChanged: (Bool) -> Void 17 | var formatter: Formatter? 18 | var text: Binding? 19 | var value: Binding? 20 | var isFirstResponder: Binding? 21 | var isSecureTextEntry: Bool? 22 | 23 | public var body: View { 24 | EmptyView() 25 | } 26 | 27 | /// Create an instance which binds with a value of type `T`. 28 | /// 29 | /// - Parameters: 30 | /// - title: The title of `self`, used as a placeholder. 31 | /// - value: The underlying value to be edited. 32 | /// - formatter: The `Formatter` to use when converting between the 33 | /// `String` the user edits and the underlying value of type `T`. 34 | /// In the event that `formatter` is unable to perform the conversion, 35 | /// `binding.value` will not be modified. 36 | /// - onEditingChanged: An `Action` that will be called when the user 37 | /// begins editing `text` and after the user finishes editing `text`, 38 | /// passing a `Bool` indicating whether `self` is currently being edited 39 | /// or not. 40 | /// - onCommit: The action to perform when the user performs an action 41 | /// (usually the return key) while the `TextField` has focus. 42 | public init(_ title: String, value: Binding, formatter: Formatter, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) { 43 | self.title = title 44 | self.onEditingChanged = onEditingChanged 45 | self.onCommit = onCommit 46 | self.value = value 47 | self.formatter = formatter 48 | } 49 | 50 | /// Sets if this view is the first responder or not. 51 | /// 52 | /// Setting a value of `true` will make this view become the first 53 | /// responder if not already. 54 | /// Setting a value of `false` will make this view resign beign first 55 | /// responder if it is the first responder. 56 | /// 57 | /// - important: Not SwiftUI compatible. 58 | public func firstResponder(_ firstResponder: Binding) -> Self { 59 | var view = self 60 | view.isFirstResponder = firstResponder 61 | return view 62 | } 63 | } 64 | 65 | extension TextField where T == String { 66 | /// Creates an instance with a a value of type `String`. 67 | /// 68 | /// - Parameters: 69 | /// - title: The title of `self`, used as a placeholder. 70 | /// - text: The text to be displayed and edited. 71 | /// - isSecureTextEntry: Specifies if text entry will be masked. 72 | /// - onEditingChanged: An `Action` that will be called when the user 73 | /// begins editing `text` and after the user finishes editing `text`, 74 | /// passing a `Bool` indicating whether `self` is currently being edited 75 | /// or not. 76 | /// - onCommit: The action to perform when the user performs an action 77 | /// (usually the return key) while the `TextField` has focus. 78 | public init(_ title: String, text: Binding, isSecureTextEntry: Bool = false, onEditingChanged: @escaping (Bool) -> Void = { _ in }, onCommit: @escaping () -> Void = {}) { 79 | self.title = title 80 | self.onEditingChanged = onEditingChanged 81 | self.onCommit = onCommit 82 | self.text = text 83 | self.isSecureTextEntry = isSecureTextEntry 84 | } 85 | } 86 | 87 | extension TextField: Renderable { 88 | public func createView(context: Context) -> UIView { 89 | let view = SwiftUITextField().noAutoresizingMask() 90 | view.adjustsFontForContentSizeCategory = true 91 | updateView(view, context: context) 92 | return view 93 | } 94 | public func updateView(_ view: UIView, context: Context) { 95 | guard let view = view as? SwiftUITextField else { return } 96 | 97 | view.value = value 98 | view.textBinding = text 99 | view.formatter = formatter 100 | view.onCommit = onCommit 101 | view.onEditingChanged = onEditingChanged 102 | view.firstResponder = isFirstResponder 103 | view.placeholder = title 104 | view.textContentType = viewStore.textContentType 105 | 106 | if view.keyboardType == .emailAddress { 107 | view.autocorrectionType = .no 108 | view.autocapitalizationType = .none 109 | } 110 | if let autocapitalization = context.viewValues?.autocapitalization { 111 | view.autocapitalizationType = autocapitalization 112 | } 113 | if let disableAutocorrection = context.viewValues?.disableAutocorrection { 114 | view.autocorrectionType = disableAutocorrection ? .no : .yes 115 | } 116 | if let text = text?.wrappedValue, view.lastWrittenText != text { 117 | view.text = text 118 | } 119 | if let text = formatter?.string(for: value?.wrappedValue), view.lastWrittenText != text { 120 | view.text = text 121 | } 122 | if let fgColor = context.viewValues?.foregroundColor { 123 | view.textColor = fgColor 124 | } 125 | if let textAlignment = context.viewValues?.multilineTextAlignment { 126 | view.textAlignment = textAlignment.nsTextAlignment 127 | } 128 | view.font = context.viewValues?.font?.font 129 | if let keyboardType = context.viewValues?.keyboardType { 130 | view.keyboardType = keyboardType 131 | } 132 | if let firstResponder = isFirstResponder { 133 | if firstResponder.wrappedValue { 134 | if !view.isFirstResponder { 135 | view.becomeFirstResponder() 136 | } 137 | } else { 138 | if view.isFirstResponder { 139 | view.resignFirstResponder() 140 | } 141 | } 142 | } 143 | if let isSecureTextEntry = isSecureTextEntry { 144 | view.isSecureTextEntry = isSecureTextEntry 145 | } 146 | } 147 | } 148 | --------------------------------------------------------------------------------