()
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 |
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 |
--------------------------------------------------------------------------------