diff --git a/Builder.xcodeproj/project.pbxproj b/Builder.xcodeproj/project.pbxproj index ac559a6..e62deb1 100644 --- a/Builder.xcodeproj/project.pbxproj +++ b/Builder.xcodeproj/project.pbxproj @@ -66,6 +66,7 @@ 4C8B50B9275FAB94004FFC15 /* UITextField+Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8B50B8275FAB94004FFC15 /* UITextField+Styles.swift */; }; 4C8B50BB275FAC93004FFC15 /* MetaTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8B50BA275FAC93004FFC15 /* MetaTextField.swift */; }; 4C8B50BD275FED84004FFC15 /* TextField+Styles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8B50BC275FED84004FFC15 /* TextField+Styles.swift */; }; + 4C915525276D2CA3009EBA64 /* ScrollingTabBarTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C915524276D2CA3009EBA64 /* ScrollingTabBarTest.swift */; }; 4C9CC36525B4F078002BE06D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9CC36425B4F078002BE06D /* AppDelegate.swift */; }; 4C9CC36725B4F078002BE06D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9CC36625B4F078002BE06D /* SceneDelegate.swift */; }; 4C9CC36925B4F078002BE06D /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9CC36825B4F078002BE06D /* MainViewController.swift */; }; @@ -195,6 +196,7 @@ 4C8B50B8275FAB94004FFC15 /* UITextField+Styles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+Styles.swift"; sourceTree = ""; }; 4C8B50BA275FAC93004FFC15 /* MetaTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetaTextField.swift; sourceTree = ""; }; 4C8B50BC275FED84004FFC15 /* TextField+Styles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TextField+Styles.swift"; sourceTree = ""; }; + 4C915524276D2CA3009EBA64 /* ScrollingTabBarTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollingTabBarTest.swift; sourceTree = ""; }; 4C9CC36125B4F078002BE06D /* Builder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Builder.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4C9CC36425B4F078002BE06D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 4C9CC36625B4F078002BE06D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -405,6 +407,7 @@ 4C1F921526E820670075A5F4 /* README.md */, 4C288FB825B6129D004D54AC /* Configuration */, 4C9CC3B025B4F26E002BE06D /* Application */, + 4C9CC3B225B4F2D4002BE06D /* Builder */, 4C9CC3EA25B4F618002BE06D /* Models */, 4C9CC3AC25B4F246002BE06D /* Resources */, 4CD0522825B544180099B277 /* Services */, @@ -466,7 +469,6 @@ 4C9CC3B125B4F29A002BE06D /* Shared */ = { isa = PBXGroup; children = ( - 4C9CC3B225B4F2D4002BE06D /* Builder */, 4C9CC3E425B4F47C002BE06D /* Extensions */, 4C8B50A7275FA61C004FFC15 /* Fields */, 4C9CC3D225B4F2DF002BE06D /* Networking */, @@ -566,6 +568,7 @@ 4CA19EE2273E0B5D00EE7433 /* TestViewController.swift */, 4C4AD771275189E000EF12C6 /* TestViews.swift */, 4C7C3FD0276A527E00F697BA /* TabBarTest.swift */, + 4C915524276D2CA3009EBA64 /* ScrollingTabBarTest.swift */, ); path = Test; sourceTree = ""; @@ -769,6 +772,7 @@ 4C9CC3FE25B5099D002BE06D /* UserService.swift in Sources */, 4C8B50B0275FA656004FFC15 /* TextFieldBehaviorAggregator.swift in Sources */, 4CC5D7A2270D1A80003137BD /* TestServices.swift in Sources */, + 4C915525276D2CA3009EBA64 /* ScrollingTabBarTest.swift in Sources */, 4CF1A64326B399D800E26446 /* Functions+Extensions.swift in Sources */, 4C9CC3C225B4F2D4002BE06D /* Builder+ScrollView.swift in Sources */, 4C47033226CB17EE006B6DEC /* Builder+Padding.swift in Sources */, diff --git a/Builder/Shared/Builder/Builder+Attributes.swift b/Builder/Builder/Builder+Attributes.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Attributes.swift rename to Builder/Builder/Builder+Attributes.swift diff --git a/Builder/Shared/Builder/Builder+Button.swift b/Builder/Builder/Builder+Button.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Button.swift rename to Builder/Builder/Builder+Button.swift diff --git a/Builder/Shared/Builder/Builder+Constraints.swift b/Builder/Builder/Builder+Constraints.swift similarity index 88% rename from Builder/Shared/Builder/Builder+Constraints.swift rename to Builder/Builder/Builder+Constraints.swift index 6c0eb9e..2b30d2e 100644 --- a/Builder/Shared/Builder/Builder+Constraints.swift +++ b/Builder/Builder/Builder+Constraints.swift @@ -104,80 +104,83 @@ extension UIView { case bottomRight } - public func embed(_ view: UIView, padding: UIEdgeInsets? = nil, safeArea: Bool = false) { - self.addConstrainedSubview(view, position: .fill, padding: padding ?? .zero, safeArea: safeArea) - } - public func embed(_ view: View, padding: UIEdgeInsets? = nil, safeArea: Bool = false) { self.addConstrainedSubview(view.build(), position: .fill, padding: padding ?? .zero, safeArea: safeArea) } - public func embed(in view: View, padding: UIEdgeInsets? = nil, safeArea: Bool = false) { - view.build().addConstrainedSubview(self, position: .fill, padding: padding ?? .zero, safeArea: safeArea) - } - public func addConstrainedSubview(_ view: UIView, position: EmbedPosition, padding: UIEdgeInsets, safeArea: Bool = false) { view.translatesAutoresizingMaskIntoConstraints = false - addSubview(view) + addVerticalConstraints(view, position: position, padding: padding, safeArea: safeArea) + addHorizontalConstraints(view, position: position, padding: padding, safeArea: safeArea) + } + private func addVerticalConstraints(_ view: UIView, position: EmbedPosition, padding: UIEdgeInsets, safeArea: Bool) { let guides: UIViewAnchoring = safeArea ? safeAreaLayoutGuide : self if [EmbedPosition.center, .centerLeft, .centerRight].contains(position) { view.centerYAnchor.constraint(equalTo: guides.centerYAnchor) + .identifier("centerY") .activate() } else { // top if [EmbedPosition.fill, .top, .left, .right, .topLeft, .topCenter, .topRight].contains(position) { view.topAnchor.constraint(equalTo: guides.topAnchor, constant: padding.top) + .identifier("top") .activate() } else { view.topAnchor.constraint(lessThanOrEqualTo: guides.topAnchor, constant: padding.top) .priority(.defaultHigh) + .identifier("top") .activate() } // bottom if [EmbedPosition.fill, .bottom, .left, .right, .bottomLeft, .bottomCenter, .bottomRight].contains(position) { view.bottomAnchor.constraint(equalTo: guides.bottomAnchor, constant: -padding.bottom) + .identifier("bottom") .activate() } else { view.bottomAnchor.constraint(greaterThanOrEqualTo: guides.bottomAnchor, constant: -padding.bottom) .priority(.defaultHigh) + .identifier("bottom") .activate() } } + } + + private func addHorizontalConstraints(_ view: UIView, position: EmbedPosition, padding: UIEdgeInsets, safeArea: Bool = false) { + let guides: UIViewAnchoring = safeArea ? safeAreaLayoutGuide : self if [EmbedPosition.center, .topCenter, .bottomCenter].contains(position) { view.centerXAnchor.constraint(equalTo: guides.centerXAnchor) + .identifier("centerX") .activate() } else { // left if [EmbedPosition.fill, .left, .top, .bottom, .topLeft, .centerLeft, .bottomLeft].contains(position) { view.leftAnchor.constraint(equalTo: guides.leftAnchor, constant: padding.left) + .identifier("left") .activate() } else { view.leftAnchor.constraint(lessThanOrEqualTo: guides.leftAnchor, constant: padding.left) .priority(.defaultHigh) + .identifier("left") .activate() } // right if [EmbedPosition.fill, .right, .top, .bottom, .topRight, .centerRight, .bottomRight].contains(position) { view.rightAnchor.constraint(equalTo: guides.rightAnchor, constant: -padding.right) + .identifier("right") .activate() } else { view.rightAnchor.constraint(greaterThanOrEqualTo: guides.rightAnchor, constant: -padding.right) .priority(.defaultHigh) + .identifier("right") .activate() } } - - } - - // deprecated - public func addSubviewWithConstraints(_ view: View, _ padding: UIEdgeInsets?, _ safeArea: Bool) { - addConstrainedSubview(view.build(), position: .fill, padding: padding ?? .zero, safeArea: safeArea) } } diff --git a/Builder/Shared/Builder/Builder+Container.swift b/Builder/Builder/Builder+Container.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Container.swift rename to Builder/Builder/Builder+Container.swift diff --git a/Builder/Shared/Builder/Builder+Context.swift b/Builder/Builder/Builder+Context.swift similarity index 94% rename from Builder/Shared/Builder/Builder+Context.swift rename to Builder/Builder/Builder+Context.swift index 59318f4..c58afc6 100644 --- a/Builder/Shared/Builder/Builder+Context.swift +++ b/Builder/Builder/Builder+Context.swift @@ -52,8 +52,14 @@ extension ViewBuilderContextProvider { view.transition(to: viewController, delay: delay) } +} + +// some utilility operations + +extension ViewBuilderContextProvider { + public func endEditing() { - view.rootView().endEditing(true) + view.rootview.firstSubview(where: { $0.isFirstResponder })?.resignFirstResponder() } public var disposeBag: DisposeBag { diff --git a/Builder/Shared/Builder/Builder+Controls.swift b/Builder/Builder/Builder+Controls.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Controls.swift rename to Builder/Builder/Builder+Controls.swift diff --git a/Builder/Shared/Builder/Builder+Divider.swift b/Builder/Builder/Builder+Divider.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Divider.swift rename to Builder/Builder/Builder+Divider.swift diff --git a/Builder/Shared/Builder/Builder+Dynamic.swift b/Builder/Builder/Builder+Dynamic.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Dynamic.swift rename to Builder/Builder/Builder+Dynamic.swift diff --git a/Builder/Shared/Builder/Builder+Extensions.swift b/Builder/Builder/Builder+Extensions.swift similarity index 67% rename from Builder/Shared/Builder/Builder+Extensions.swift rename to Builder/Builder/Builder+Extensions.swift index 864d0ed..44a80a2 100644 --- a/Builder/Shared/Builder/Builder+Extensions.swift +++ b/Builder/Builder/Builder+Extensions.swift @@ -28,7 +28,7 @@ extension UIView { public func reset(_ view: View, padding: UIEdgeInsets? = nil, safeArea: Bool = false) { let existingSubviews = subviews - addSubviewWithConstraints(view.build(), padding, safeArea) + addConstrainedSubview(view.build(), position: .fill, padding: padding ?? .zero, safeArea: safeArea) existingSubviews.forEach { $0.removeFromSuperview() } } @@ -62,73 +62,84 @@ extension UIView { // deprecated version @discardableResult public func embedModified(_ view: V, padding: UIEdgeInsets? = nil, safeArea: Bool = false, _ modifier: (_ view: V) -> Void) -> V { - addSubviewWithConstraints(view, padding, safeArea) + addConstrainedSubview(view, position: .fill, padding: padding ?? .zero, safeArea: safeArea) modifier(view) return view } } -extension UIResponder { - public var parentViewController: UIViewController? { - return next as? UIViewController ?? next?.parentViewController - } -} - extension UIView { // goes to top of view chain, then initiates full search of view tree public func find(_ key: K) -> UIView? where K.RawValue == Int { - recursiveFind(key.rawValue, keyPath: \.tag, in: rootView()) + rootview.firstSubview(where: { $0.tag == key.rawValue }) } public func find(_ key: K) -> UIView? where K.RawValue == String { - recursiveFind(key.rawValue, keyPath: \.accessibilityIdentifier, in: rootView()) + rootview.firstSubview(where: { $0.accessibilityIdentifier == key.rawValue }) } // searches down the tree looking for identifier public func find(subview key: K) -> UIView? where K.RawValue == Int { - recursiveFind(key.rawValue, keyPath: \.tag, in: self) + firstSubview(where: { $0.tag == key.rawValue }) } public func find(subview key: K) -> UIView? where K.RawValue == String { - recursiveFind(key.rawValue, keyPath: \.accessibilityIdentifier, in: self) + firstSubview(where: { $0.accessibilityIdentifier == key.rawValue }) } // searches up the tree looking for identifier in superview path public func find(superview key: K) -> UIView? where K.RawValue == Int { - superviewFind(key.rawValue, keyPath: \.tag) + firstSuperview(where: { $0.tag == key.rawValue }) } public func find(superview key: K) -> UIView? where K.RawValue == String { - superviewFind(key.rawValue, keyPath: \.accessibilityIdentifier) + firstSuperview(where: { $0.accessibilityIdentifier == key.rawValue }) } - internal func rootView() -> UIView { - var root = self.superview ?? self - while let view = root.superview { root = view} - return root - } +} + +extension UIView { - internal func recursiveFind(_ key: T, keyPath: KeyPath, in view: UIView) -> UIView? { - if view[keyPath: keyPath] == key { - return view + public func scrollIntoView() { + guard let scrollview = firstSuperview(where: { $0 is UIScrollView }) as? UIScrollView else { + return } - for child in view.subviews { - if let foundView = recursiveFind(key, keyPath: keyPath, in: child) { - return foundView + let visible = convert(frame, to: scrollview) + UIViewPropertyAnimator(duration: 0.1, curve: .linear) { + scrollview.scrollRectToVisible(visible, animated: false) + }.startAnimation() + } + +} + +extension UIView { + + public func firstSubview(where predicate: (_ view: UIView) -> Bool) -> UIView? { + for child in subviews { + if predicate(child) { + return child + } else if let found = child.firstSubview(where: predicate){ + return found } } return nil } - internal func superviewFind(_ key: T, keyPath: KeyPath) -> UIView? { - var parent: UIView? = self - while let view = parent { - if view[keyPath: keyPath] == key { - return view - } - parent = view.superview + public func firstSuperview(where predicate: (_ view: UIView) -> Bool) -> UIView? { + if let parent = superview { + return predicate(parent) ? parent : parent.firstSuperview(where: predicate) } return nil } + public var rootview: UIView { + firstSuperview(where: { $0.superview == nil }) ?? self + } + +} + +extension UIResponder { + public var parentViewController: UIViewController? { + return next as? UIViewController ?? next?.parentViewController + } } extension Int: RawRepresentable { diff --git a/Builder/Shared/Builder/Builder+ForEach.swift b/Builder/Builder/Builder+ForEach.swift similarity index 100% rename from Builder/Shared/Builder/Builder+ForEach.swift rename to Builder/Builder/Builder+ForEach.swift diff --git a/Builder/Shared/Builder/Builder+Gestures.swift b/Builder/Builder/Builder+Gestures.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Gestures.swift rename to Builder/Builder/Builder+Gestures.swift diff --git a/Builder/Shared/Builder/Builder+Group.swift b/Builder/Builder/Builder+Group.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Group.swift rename to Builder/Builder/Builder+Group.swift diff --git a/Builder/Shared/Builder/Builder+Image.swift b/Builder/Builder/Builder+Image.swift similarity index 99% rename from Builder/Shared/Builder/Builder+Image.swift rename to Builder/Builder/Builder+Image.swift index c671ffa..cdf5c62 100644 --- a/Builder/Shared/Builder/Builder+Image.swift +++ b/Builder/Builder/Builder+Image.swift @@ -13,7 +13,7 @@ import RxSwift public struct ImageView: ModifiableView { public let modifiableView = Modified(UIImageView()) - + // lifecycle public init(_ image: UIImage?) { modifiableView.image = image diff --git a/Builder/Shared/Builder/Builder+Label.swift b/Builder/Builder/Builder+Label.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Label.swift rename to Builder/Builder/Builder+Label.swift diff --git a/Builder/Shared/Builder/Builder+Navigation.swift b/Builder/Builder/Builder+Navigation.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Navigation.swift rename to Builder/Builder/Builder+Navigation.swift diff --git a/Builder/Shared/Builder/Builder+Padding.swift b/Builder/Builder/Builder+Padding.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Padding.swift rename to Builder/Builder/Builder+Padding.swift diff --git a/Builder/Shared/Builder/Builder+RxSwift.swift b/Builder/Builder/Builder+RxSwift.swift similarity index 100% rename from Builder/Shared/Builder/Builder+RxSwift.swift rename to Builder/Builder/Builder+RxSwift.swift diff --git a/Builder/Shared/Builder/Builder+ScrollView.swift b/Builder/Builder/Builder+ScrollView.swift similarity index 78% rename from Builder/Shared/Builder/Builder+ScrollView.swift rename to Builder/Builder/Builder+ScrollView.swift index fdbc956..1ecad3c 100644 --- a/Builder/Shared/Builder/Builder+ScrollView.swift +++ b/Builder/Builder/Builder+ScrollView.swift @@ -17,11 +17,15 @@ public struct ScrollView: ModifiableView { public init(_ view: View?, padding: UIEdgeInsets? = nil, safeArea: Bool = false) { guard let view = view else { return } - modifiableView.embed(view, padding: padding, safeArea: safeArea) + modifiableView.views = [view] + modifiableView.padding = padding ?? .zero + modifiableView.safeArea = safeArea } public init(padding: UIEdgeInsets? = nil, safeArea: Bool = false, @ViewResultBuilder _ builder: () -> ViewConvertable) { - builder().asViews().forEach { modifiableView.embed($0, padding: padding, safeArea: safeArea) } + modifiableView.views = builder() + modifiableView.padding = padding ?? .zero + modifiableView.safeArea = safeArea } } @@ -77,7 +81,9 @@ public struct VerticalScrollView: ModifiableView { } public init(padding: UIEdgeInsets? = nil, safeArea: Bool = false, @ViewResultBuilder _ builder: () -> ViewConvertable) { - builder().asViews().forEach { modifiableView.embed($0, padding: padding, safeArea: safeArea) } + modifiableView.views = builder() + modifiableView.padding = padding ?? .zero + modifiableView.safeArea = safeArea } } @@ -86,6 +92,11 @@ public class BuilderInternalScrollView: UIScrollView, UIScrollViewDelegate { public var scrollViewDidScrollHandler: ((_ context: ViewBuilderContext) -> Void)? + fileprivate var views: ViewConvertable? + fileprivate var padding: UIEdgeInsets = .zero + fileprivate var position: EmbedPosition = .fill + fileprivate var safeArea: Bool = false + @objc public func scrollViewDidScroll(_ scrollView: UIScrollView) { scrollViewDidScrollHandler?(ViewBuilderContext(view: self)) } @@ -94,6 +105,17 @@ public class BuilderInternalScrollView: UIScrollView, UIScrollViewDelegate { optionalBuilderAttributes()?.commonDidMoveToWindow(self) } + override public func didMoveToSuperview() { + views?.asViews().forEach { + let view = $0.build() + let attributes = view.optionalBuilderAttributes() + let position = attributes?.position ?? position + let padding = attributes?.insets ?? padding + addConstrainedSubview(view, position: position, padding: padding, safeArea: safeArea) + } + super.didMoveToSuperview() + } + } public class BuilderVerticalScrollView: BuilderInternalScrollView { diff --git a/Builder/Shared/Builder/Builder+Spacer.swift b/Builder/Builder/Builder+Spacer.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Spacer.swift rename to Builder/Builder/Builder+Spacer.swift diff --git a/Builder/Shared/Builder/Builder+Stack.swift b/Builder/Builder/Builder+Stack.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Stack.swift rename to Builder/Builder/Builder+Stack.swift diff --git a/Builder/Shared/Builder/Builder+Styles.swift b/Builder/Builder/Builder+Styles.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Styles.swift rename to Builder/Builder/Builder+Styles.swift diff --git a/Builder/Shared/Builder/Builder+Switch.swift b/Builder/Builder/Builder+Switch.swift similarity index 100% rename from Builder/Shared/Builder/Builder+Switch.swift rename to Builder/Builder/Builder+Switch.swift diff --git a/Builder/Shared/Builder/Builder+TableView.swift b/Builder/Builder/Builder+TableView.swift similarity index 100% rename from Builder/Shared/Builder/Builder+TableView.swift rename to Builder/Builder/Builder+TableView.swift diff --git a/Builder/Shared/Builder/Builder+TextField.swift b/Builder/Builder/Builder+TextField.swift similarity index 87% rename from Builder/Shared/Builder/Builder+TextField.swift rename to Builder/Builder/Builder+TextField.swift index c7f2a6b..de3fdc1 100644 --- a/Builder/Shared/Builder/Builder+TextField.swift +++ b/Builder/Builder/Builder+TextField.swift @@ -150,9 +150,10 @@ extension ModifiableView where Base: UITextField { extension ModifiableView where Base: UITextField { @discardableResult - public func onChange(_ handler: @escaping (_ context: ViewBuilderValueContext) -> Void) -> ViewModifier { + public func onControlEvent(_ event: UIControl.Event, + handler: @escaping (_ context: ViewBuilderValueContext) -> Void) -> ViewModifier { ViewModifier(modifiableView) { - $0.rx.controlEvent(.editingChanged) + $0.rx.controlEvent([event]) .observe(on: MainScheduler.instance) .subscribe(onNext: { [unowned modifiableView] () in handler(ViewBuilderValueContext(view: modifiableView, value: modifiableView.text)) @@ -161,28 +162,24 @@ extension ModifiableView where Base: UITextField { } } + @discardableResult + public func onChange(_ handler: @escaping (_ context: ViewBuilderValueContext) -> Void) -> ViewModifier { + onControlEvent(.editingChanged, handler: handler) + } + + @discardableResult + public func onEditingDidBegin(_ handler: @escaping (_ context: ViewBuilderValueContext) -> Void) -> ViewModifier { + onControlEvent(.editingDidBegin, handler: handler) + } + @discardableResult public func onEditingDidEnd(_ handler: @escaping (_ context: ViewBuilderValueContext) -> Void) -> ViewModifier { - ViewModifier(modifiableView) { - $0.rx.controlEvent([.editingDidEnd]) - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [unowned modifiableView] () in - handler(ViewBuilderValueContext(view: modifiableView, value: modifiableView.text)) - }) - .disposed(by: $0.rxDisposeBag) - } + onControlEvent(.editingDidEnd, handler: handler) } @discardableResult public func onEditingDidEndOnExit(_ handler: @escaping (_ context: ViewBuilderValueContext) -> Void) -> ViewModifier { - ViewModifier(modifiableView) { - $0.rx.controlEvent([.editingDidEndOnExit]) - .observe(on: MainScheduler.instance) - .subscribe(onNext: { [unowned modifiableView] () in - handler(ViewBuilderValueContext(view: modifiableView, value: modifiableView.text)) - }) - .disposed(by: $0.rxDisposeBag) - } + onControlEvent(.editingDidEndOnExit, handler: handler) } } diff --git a/Builder/Shared/Builder/Builder+Variable.swift b/Builder/Builder/Builder+Variable.swift similarity index 91% rename from Builder/Shared/Builder/Builder+Variable.swift rename to Builder/Builder/Builder+Variable.swift index e215276..0088523 100644 --- a/Builder/Shared/Builder/Builder+Variable.swift +++ b/Builder/Builder/Builder+Variable.swift @@ -9,7 +9,6 @@ import Foundation import RxSwift import RxCocoa - @propertyWrapper public struct Variable { private var relay: BehaviorRelay @@ -46,11 +45,6 @@ extension Variable where T:Equatable { .subscribe { observer($0) } } -// public func onChange(_ bag: DisposeBag, _ observer: @escaping (_ value: T) -> ()) { -// onChange(observer) -// .disposed(by: bag) -// } - } extension Variable: RxBinding { @@ -76,7 +70,6 @@ extension Variable: RxBidirectionalBinding { } - //struct A: ViewBuilder { // @Variable var name = "Michael" // var body: View { diff --git a/Builder/Shared/Builder/Builder+View.swift b/Builder/Builder/Builder+View.swift similarity index 100% rename from Builder/Shared/Builder/Builder+View.swift rename to Builder/Builder/Builder+View.swift diff --git a/Builder/Shared/Builder/Builder+ViewController.swift b/Builder/Builder/Builder+ViewController.swift similarity index 100% rename from Builder/Shared/Builder/Builder+ViewController.swift rename to Builder/Builder/Builder+ViewController.swift diff --git a/Builder/Shared/Builder/Builder+With.swift b/Builder/Builder/Builder+With.swift similarity index 100% rename from Builder/Shared/Builder/Builder+With.swift rename to Builder/Builder/Builder+With.swift diff --git a/Builder/Shared/Builder/Builder+ZStack.swift b/Builder/Builder/Builder+ZStack.swift similarity index 100% rename from Builder/Shared/Builder/Builder+ZStack.swift rename to Builder/Builder/Builder+ZStack.swift diff --git a/Builder/Shared/Builder/Builder.swift b/Builder/Builder/Builder.swift similarity index 100% rename from Builder/Shared/Builder/Builder.swift rename to Builder/Builder/Builder.swift diff --git a/Builder/ContactForm/FABMenuView.swift b/Builder/ContactForm/FABMenuView.swift index b0a6c02..b92e0d3 100644 --- a/Builder/ContactForm/FABMenuView.swift +++ b/Builder/ContactForm/FABMenuView.swift @@ -33,11 +33,7 @@ struct FABMenuView: ViewBuilder { .backgroundColor(.red) .position(.topCenter) .onTapGesture { context in - // UIView.animate(withDuration: 1.5) { showFABMenu.toggle() - // } completion: { _ in - // // - // } } } .position(.bottomCenter) @@ -51,7 +47,7 @@ struct FABMenuView: ViewBuilder { // close area ZStackView { ContainerView() - .backgroundColor(.blue) + .backgroundColor(.black) .position(.bottom) .height(25) @@ -84,20 +80,13 @@ struct FABMenuView: ViewBuilder { .padding(20) .spacing(0) } - .backgroundColor(.blue) + .backgroundColor(.black) .position(.bottom) } .spacing(0) } .backgroundColor(UIColor(white: 0.5, alpha: 0.3)) .width(UIScreen.main.bounds.width) - // .onReceive($showFABMenu.asObservable(), handler: { context in - // UIView.animate(withDuration: 0.2) { - // context.view.isHidden = !context.value - // } completion: { _ in - // // - // } - // }) .hidden(bind: $showFABMenu.asObservable().map { !$0 }) .onTapGesture { context in showFABMenu.toggle() diff --git a/Builder/ContactForm/FormFieldManager.swift b/Builder/ContactForm/FormFieldManager.swift index 359c974..6f54618 100644 --- a/Builder/ContactForm/FormFieldManager.swift +++ b/Builder/ContactForm/FormFieldManager.swift @@ -72,6 +72,9 @@ extension MetaTextField { .placeholder(manager.placeholder(for: id)) .error(bind: manager.error(for: id)) .returnKeyType(.next) + .onEditingDidBegin({ context in + context.view.scrollIntoView() + }) .onEditingDidEndOnExit { [weak manager] context in if let id = manager?.nextID(from: id), let field = context.find(id), field.canBecomeFirstResponder { field.becomeFirstResponder() diff --git a/Builder/DetailCardView.swift b/Builder/DetailCardView.swift index 098f522..e27f9d4 100644 --- a/Builder/DetailCardView.swift +++ b/Builder/DetailCardView.swift @@ -21,7 +21,7 @@ struct DetailCardView: ViewBuilder { DLSCardView { VStackView { DetailPhotoView(photo: viewModel.photo(), name: viewModel.fullname) - .height(250) + VStackView { NameValueView(name: "Address", value: viewModel.street) NameValueView(name: "", value: viewModel.cityStateZip) @@ -55,6 +55,7 @@ struct DetailPhotoView: ViewBuilder { ImageView(photo) .contentMode(.scaleAspectFill) .clipsToBounds(true) + .height(300) LabelView(name) .alignment(.right) .font(.title2) diff --git a/Builder/Menu/MenuViewController.swift b/Builder/Menu/MenuViewController.swift index fdca734..4f530aa 100644 --- a/Builder/Menu/MenuViewController.swift +++ b/Builder/Menu/MenuViewController.swift @@ -21,7 +21,8 @@ class MenuViewController: UIViewController { view.backgroundColor = .systemBackground view.embed(MenuTableView()) - push(CustomTabBarViewController()) +// push(CustomTabBarViewController()) + push(ScrollingTabBarViewController()) } } diff --git a/Builder/Services/Dismissible.swift b/Builder/Services/Dismissible.swift index bcb0c86..e01a20d 100644 --- a/Builder/Services/Dismissible.swift +++ b/Builder/Services/Dismissible.swift @@ -58,6 +58,7 @@ class Dismissible { doAction() } + // swiftlint:disable:next cyclomatic_complexity private func doAction() { guard let vc = viewController else { return } switch action { diff --git a/Builder/Test/ScrollingTabBarTest.swift b/Builder/Test/ScrollingTabBarTest.swift new file mode 100644 index 0000000..26f1a21 --- /dev/null +++ b/Builder/Test/ScrollingTabBarTest.swift @@ -0,0 +1,148 @@ +// +// TabBarTest.swift +// Builder +// +// Created by Michael Long on 12/15/21. +// + +import UIKit +import RxSwift + +class ScrollingTabBarViewController: UIViewController { + + @Variable var selectedTab: Int = 0 + + var tabs = [ + "Accounts", + "Transactions", + "Balances", + "Loans", + "Credit Cards", + "IRAs/401Ks", + ] + + override func viewDidLoad() { + super.viewDidLoad() + title = "Scrolling Tab Bar" + view.backgroundColor = .secondarySystemBackground + view.embed(content()) + } + + func content() -> View { + ZStackView { + ContainerView() + .insets(top: 40, left: 0, bottom: 0, right: 0) + .onReceive($selectedTab) { context in + context.transition(to: CustomTestView(tab: context.value)) + } + ScrollingTabBarView(selectedTab: $selectedTab, tabs: tabs) + .position(.top) + } + } + +} + +private struct CustomTestView: ViewBuilder { + let tab: Int + var body: View { + VerticalScrollView { + VStackView { + ForEach(30) { row in + LabelView("Tab \(tab+1) is selected.") + .onTapGesture { context in + context.push(CustomDetailsView(title: "Details for tab \(tab+1), row \(row+1)")) + } + } + SpacerView() + } + .padding(20) + } + } +} + +private struct CustomDetailsView: ViewBuilder { + let title: String + var body: View { + VStackView { + LabelView(title) + } + .alignment(.center) + .onAppearOnce { context in + context.viewController?.navigationItem.title = "Details" + } + } +} + +struct ScrollingTabBarView: ViewBuilder { + + @Variable var selectedTab: Int + let tabs: [String] + + let HEIGHT: CGFloat = 44 + let INDICATOR_HEIGHT: CGFloat = 10 + + var body: View { + ZStackView { + ContainerView() + .backgroundColor(.black) + .contentHuggingPriority(.defaultLow, for: .horizontal) + .position(.top) + .height(HEIGHT) + + ScrollView { + HStackView { + ForEach(tabs.count) { index in + ZStackView { + ButtonView(tabs[index]) + .identifier("TAB-\(tabs[index])") + .color(.white) + .backgroundColor(.black) + .contentHuggingPriority(.defaultLow, for: .horizontal) + .contentHuggingPriority(.defaultLow, for: .vertical) + .selected(bind: $selectedTab.asObservable().map { $0 == index }) + .padding(h: 10, v: 0) + .position(.top) + .height(HEIGHT) + .onTapGesture { context in + context.view.scrollIntoView() + selectedTab = index + } + + ContainerView() + .roundedCorners(radius: INDICATOR_HEIGHT / 2, corners: [.layerMinXMinYCorner, .layerMaxXMaxYCorner]) + .bind(keyPath: \.backgroundColor, binding: tabIndicatorColor(index)) + .height(INDICATOR_HEIGHT) + .insets(top: 0, left: 4, bottom: 0, right: 4) + .position(.bottom) + } + + .contentCompressionResistancePriority(.required, for: .horizontal) + .height(HEIGHT + (INDICATOR_HEIGHT / 2)) + } + SpacerView(width: 0) + } + .spacing(0) + } + .with { + $0.showsVerticalScrollIndicator = false + $0.showsHorizontalScrollIndicator = false + $0.bounces = false + } + } + .height(HEIGHT + (INDICATOR_HEIGHT / 2)) + } + + func tabIndicatorColor(_ index: Int) -> Observable { + $selectedTab + .asObservable() + .map { $0 == index ? .red : .clear } + } + + func tabTextColor(_ index: Int) -> Observable { + $selectedTab + .asObservable() + .map { $0 == index ? .white : .lightGray } + } + + +} diff --git a/Builder/Test/TabBarTest.swift b/Builder/Test/TabBarTest.swift index f921c82..e512c7f 100644 --- a/Builder/Test/TabBarTest.swift +++ b/Builder/Test/TabBarTest.swift @@ -38,6 +38,7 @@ class CustomTabBarViewController: UIViewController { context.transition(to: host) } CustomTabBarView(selectedTab: $selectedTab, tabs: tabs) + .position(.top) } } @@ -119,41 +120,100 @@ struct CustomTabBarView: ViewBuilder { @Variable var selectedTab: Int let tabs: [String] + let HEIGHT: CGFloat = 44 + let INDICATOR_HEIGHT: CGFloat = 12 + + var body: View { + HStackView { + ForEach(tabs.count) { index in + ZStackView { + ButtonView(tabs[index]) + .identifier("TAB-\(tabs[index])") + .color(.white) + .backgroundColor(.black) + .contentHuggingPriority(.defaultLow, for: .horizontal) + .contentHuggingPriority(.defaultLow, for: .vertical) + .selected(bind: $selectedTab.asObservable().map { $0 == index }) + .position(.top) + .height(HEIGHT) + .onTapGesture { context in + selectedTab = index + } + + ContainerView() + .roundedCorners(radius: INDICATOR_HEIGHT / 2, corners: [.layerMinXMinYCorner, .layerMaxXMaxYCorner]) + .bind(keyPath: \.backgroundColor, binding: tabIndicatorColor(index)) + .height(INDICATOR_HEIGHT) + .insets(top: 0, left: 4, bottom: 0, right: 4) + .position(.bottom) + } + } + } + .distribution(.fillEqually) + .spacing(0) + .height(HEIGHT + (INDICATOR_HEIGHT / 2)) + } + + func tabIndicatorColor(_ index: Int) -> Observable { + $selectedTab + .asObservable() + .map { $0 == index ? .red : .clear } + } + +} + +struct AnnimatingTabBarView: ViewBuilder { + + @Variable var selectedTab: Int + let tabs: [String] + + let HEIGHT: CGFloat = 44 + let INDICATOR_HEIGHT: CGFloat = 12 + var body: View { ZStackView { - ContainerView { - HStackView { - ForEach(tabs.count) { index in + HStackView { + ForEach(tabs.count) { index in + ContainerView { LabelView(tabs[index]) .alignment(.center) - .color(bind: tabTextColor(index)) + .color(.white) .contentHuggingPriority(.defaultLow, for: .horizontal) .contentHuggingPriority(.defaultLow, for: .vertical) + .padding(h: 10, v: 0) .onTapGesture { context in selectedTab = index + guard let container = context.find("tabContainer"), let underline = context.find("tabIdentifier") else { return } + let rect = context.view.convert(context.view.frame, to: container) + let left = underline.superview?.constraints + .first(where: { ($0.firstItem as? UIView) === underline && $0.identifier == "left" }) + underline.constraints.first(where: { $0.identifier == "width" })?.constant = rect.size.width - 8 + UIView.animate(withDuration: 0.2) { + left?.constant = rect.origin.x + 4 + underline.superview?.layoutIfNeeded() + } + } } + .backgroundColor(.black) } - .distribution(.fillEqually) } - .backgroundColor(.black) + .identifier("tabContainer") + .distribution(.fillEqually) + .spacing(0) .position(.top) - .height(40) + .height(HEIGHT) - HStackView { - ForEach(tabs.count) { index in - ContainerView() - .height(7) - .roundedCorners(radius: 4, corners: [.layerMinXMinYCorner, .layerMaxXMaxYCorner]) - .bind(keyPath: \.backgroundColor, binding: tabIndicatorColor(index)) - } - } - .padding(top: 0, left: 4, bottom: 0, right: 4) - .distribution(.fillEqually) - .position(.bottom) + ContainerView() + .identifier("tabIdentifier") + .roundedCorners(radius: INDICATOR_HEIGHT / 2, corners: [.layerMinXMinYCorner, .layerMaxXMaxYCorner]) + .backgroundColor(.red) + .height(INDICATOR_HEIGHT) + .insets(top: 0, left: 4, bottom: 0, right: 4) + .position(.bottomLeft) + .width(100) } - .position(.top) - .height(43, priority: .required) + .height(HEIGHT + (INDICATOR_HEIGHT / 2)) } func tabIndicatorColor(_ index: Int) -> Observable { @@ -162,10 +222,4 @@ struct CustomTabBarView: ViewBuilder { .map { $0 == index ? .red : .clear } } - func tabTextColor(_ index: Int) -> Observable { - $selectedTab - .asObservable() - .map { $0 == index ? .white : .lightGray } - } - }