Skip to content

Commit

Permalink
Support any UIView (#46)
Browse files Browse the repository at this point in the history
* Support component on generic UIViews

* rename engine to componentEngine

* Make tappableview a uiview instead

* Rename ComponentViewComponent to ViewWrapperComponent + more cleanup

* Update example and tests to not use Component

* componentView -> hostingView

* Add migration guide and other small fixes

* Move migration guides into documentations
  • Loading branch information
lkzhao authored Jul 17, 2024
1 parent fabb40c commit 6bd9624
Show file tree
Hide file tree
Showing 41 changed files with 683 additions and 571 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class AsyncImageViewController: ComponentViewController {

override func viewDidLoad() {
super.viewDidLoad()
componentView.animator = TransformAnimator()
componentView.componentEngine.animator = TransformAnimator()
title = "Async Image"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class CardViewController: ComponentViewController {

override func viewDidLoad() {
super.viewDidLoad()
componentView.animator = TransformAnimator()
componentView.componentEngine.animator = TransformAnimator()
}

override var component: any Component {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class CardViewController2: ComponentViewController {

override func viewDidLoad() {
super.viewDidLoad()
componentView.animator = TransformAnimator()
componentView.componentEngine.animator = TransformAnimator()
}

override var component: any Component {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class CardViewController3: ComponentViewController {

override func viewDidLoad() {
super.viewDidLoad()
componentView.animator = TransformAnimator()
componentView.componentEngine.animator = TransformAnimator()
}

override var component: any Component {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class ComplexLayoutViewController: ComponentViewController {
self.didTapAddTag()
}
}
.view().with(\.animator, TransformAnimator())
.view().with(\.componentEngine.animator, TransformAnimator())
Separator()
Text("Tags column")
FlexColumn(spacing: 5) {
Expand All @@ -136,7 +136,9 @@ class ComplexLayoutViewController: ComponentViewController {
self.didTapAddTag()
}
}
.size(height: 130).view().with(\.animator, TransformAnimator())
.size(height: 130)
.view()
.with(\.componentEngine.animator, TransformAnimator())

Text("Shuffle tags").textColor(.systemBlue)
.tappableView { [unowned self] in
Expand All @@ -160,11 +162,13 @@ class ComplexLayoutViewController: ComponentViewController {
}
}
.inset(top: 5, left: 10, bottom: 0, right: 10).visibleInset(-200).scrollView()
.onFirstReload { scrollView in
let cellFrame = scrollView.frame(id: ComplexLayoutViewController.defaultHorizontalListData[5].id)!
.showsHorizontalScrollIndicator(false)
.with(\.componentEngine.onFirstReload) { scrollView in
guard let scrollView = scrollView as? UIScrollView else { return }
let cellFrame = scrollView.componentEngine.frame(id: ComplexLayoutViewController.defaultHorizontalListData[5].id)!
scrollView.scrollRectToVisible(CGRect(center: cellFrame.center, size: scrollView.bounds.size), animated: false) // scroll to item 5 as the center
}
.showsHorizontalScrollIndicator(false).with(\.animator, TransformAnimator())
.with(\.componentEngine.animator, TransformAnimator())
HStack(spacing: 10) {
ViewComponent<UIButton>(view: resetButton).isEnabled(horizontalListData != ComplexLayoutViewController.defaultHorizontalListData).id("reset")
ViewComponent<UIButton>(view: shuffleButton).isEnabled(!horizontalListData.isEmpty).id("shuffled")
Expand All @@ -178,7 +182,7 @@ class ComplexLayoutViewController: ComponentViewController {

override func viewDidLoad() {
super.viewDidLoad()
componentView.animator = TransformAnimator()
componentView.componentEngine.animator = TransformAnimator()
}

@objc func resetHorizontalListData() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ struct IntroductionCard: ComponentBuilder {
}
.flex()
}
.inset(10).defaultShadow().with(\.animator, TransformAnimator()).id("introduction")
.inset(10)
.defaultShadow()
.with(\.componentEngine.animator, TransformAnimator())
.id("introduction")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class GalleryViewController: ComponentViewController {

override func viewDidLoad() {
super.viewDidLoad()
componentView.animator = TransformAnimator()
componentView.componentEngine.animator = TransformAnimator()
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
TappableViewConfiguration.default = TappableViewConfiguration { view, isHighlighted in
TappableViewConfig.default = TappableViewConfig { view, isHighlighted in
let scale: CGFloat = isHighlighted ? 0.9 : 1.0
UIView.animate(withDuration: 0.2) {
view.transform = .identity.scaledBy(x: scale, y: scale)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import UIComponent
import UIKit

class ComponentViewController: UIViewController {
let componentView = ComponentScrollView()
let componentView = UIScrollView()

override func viewDidLoad() {
super.viewDidLoad()
Expand All @@ -20,7 +20,7 @@ class ComponentViewController: UIViewController {
}

func reloadComponent() {
componentView.component = component
componentView.componentEngine.component = component
}

var component: any Component {
Expand Down
4 changes: 2 additions & 2 deletions Examples/UIComponentExample/Supporting Files/Modifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import UIKit

extension Component {

func styleColor(_ tintColor: UIColor) -> UpdateComponent<ComponentViewComponent<ComponentView>> {
func styleColor(_ tintColor: UIColor) -> UpdateComponent<ViewWrapperComponent<UIView>> {
view()
.update {
$0.backgroundColor = tintColor.withAlphaComponent(0.5)
Expand All @@ -16,7 +16,7 @@ extension Component {
}
}

func defaultShadow() -> UpdateComponent<ComponentViewComponent<ComponentView>> {
func defaultShadow() -> UpdateComponent<ViewWrapperComponent<UIView>> {
view()
.update {
$0.backgroundColor = .systemBackground
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ UIComponent is a declarative and modern framework to build user interfaces with
- [Internal Architecture](https://lkzhao.com/UIComponent/documentation/uicomponent/architecture)
- [Compare to SwiftUI](https://lkzhao.com/UIComponent/documentation/uicomponent/swiftuicompare)
- [Full API Reference](https://lkzhao.com/UIComponent/documentation/uicomponent)
- [Version 2.0 Migration Guide](Version2MigrationGuide.md)
- [Migration Guides](https://lkzhao.com/UIComponent/documentation/uicomponent/migrationguides)

> Checkout the [Examples](https://github.com/lkzhao/UIComponent/tree/master/Examples) folder for code examples.
14 changes: 7 additions & 7 deletions Sources/UIComponent/Animators/TransformAnimator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ public struct TransformAnimator: Animator {
self.cascade = cascade
}

public func delete(componentView: ComponentDisplayableView, view: UIView, completion: @escaping () -> Void) {
if componentView.isReloading, componentView.bounds.intersects(view.frame) {
public func delete(hostingView: UIView, view: UIView, completion: @escaping () -> Void) {
if hostingView.componentEngine.isReloading, hostingView.bounds.intersects(view.frame) {
UIView.animate(
withDuration: duration,
delay: 0,
Expand All @@ -43,7 +43,7 @@ public struct TransformAnimator: Animator {
view.alpha = 0
},
completion: { _ in
if !componentView.visibleViews.contains(view) {
if !hostingView.componentEngine.visibleViews.contains(view) {
view.transform = CGAffineTransform.identity
view.alpha = 1
}
Expand All @@ -55,11 +55,11 @@ public struct TransformAnimator: Animator {
}
}

public func insert(componentView: ComponentDisplayableView, view: UIView, frame: CGRect) {
public func insert(hostingView: UIView, view: UIView, frame: CGRect) {
view.bounds.size = frame.size
view.center = frame.center
if componentView.isReloading, componentView.hasReloaded, componentView.bounds.intersects(frame) {
let offsetTime: TimeInterval = cascade ? TimeInterval(frame.origin.distance(componentView.bounds.origin) / 3000) : 0
if hostingView.componentEngine.isReloading, hostingView.componentEngine.hasReloaded, hostingView.bounds.intersects(frame) {
let offsetTime: TimeInterval = cascade ? TimeInterval(frame.origin.distance(hostingView.bounds.origin) / 3000) : 0
UIView.performWithoutAnimation {
view.layer.transform = transform
view.alpha = 0
Expand All @@ -78,7 +78,7 @@ public struct TransformAnimator: Animator {
}
}

public func update(componentView _: ComponentDisplayableView, view: UIView, frame: CGRect) {
public func update(hostingView _: UIView, view: UIView, frame: CGRect) {
if view.center != frame.center {
UIView.animate(
withDuration: duration,
Expand Down
34 changes: 19 additions & 15 deletions Sources/UIComponent/Animators/WrapperAnimator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,44 @@ public struct WrapperAnimator: Animator {
/// Determines whether the `WrapperAnimator` should pass the update operation to the underlying `content` animator after executing `updateBlock`.
public var passthroughUpdate: Bool = false
/// A block that is executed when a new view is inserted. If `nil`, the insert operation is passed to the underlying `content` animator.
public var insertBlock: ((ComponentDisplayableView, UIView, CGRect) -> Void)?
public var insertBlock: ((UIView, UIView, CGRect) -> Void)?
/// A block that is executed when a view needs to be updated. If `nil`, the update operation is passed to the underlying `content` animator.
public var updateBlock: ((ComponentDisplayableView, UIView, CGRect) -> Void)?
public var updateBlock: ((UIView, UIView, CGRect) -> Void)?
/// A block that is executed when a view is deleted. If `nil`, the delete operation is passed to the underlying `content` animator.
public var deleteBlock: ((ComponentDisplayableView, UIView, @escaping () -> Void) -> Void)?
public var deleteBlock: ((UIView, UIView, @escaping () -> Void) -> Void)?

public func shift(componentView: ComponentDisplayableView, delta: CGPoint, view: UIView) {
(content ?? componentView.animator).shift(componentView: componentView, delta: delta, view: view)
public func shift(hostingView: UIView, delta: CGPoint, view: UIView) {
(content ?? hostingView.componentEngine.animator).shift(
hostingView: hostingView,
delta: delta,
view: view
)
}

public func update(componentView: ComponentDisplayableView, view: UIView, frame: CGRect) {
public func update(hostingView: UIView, view: UIView, frame: CGRect) {
if let updateBlock {
updateBlock(componentView, view, frame)
updateBlock(hostingView, view, frame)
if passthroughUpdate {
(content ?? componentView.animator).update(componentView: componentView, view: view, frame: frame)
(content ?? hostingView.componentEngine.animator).update(hostingView: hostingView, view: view, frame: frame)
}
} else {
(content ?? componentView.animator).update(componentView: componentView, view: view, frame: frame)
(content ?? hostingView.componentEngine.animator).update(hostingView: hostingView, view: view, frame: frame)
}
}

public func insert(componentView: ComponentDisplayableView, view: UIView, frame: CGRect) {
public func insert(hostingView: UIView, view: UIView, frame: CGRect) {
if let insertBlock {
insertBlock(componentView, view, frame)
insertBlock(hostingView, view, frame)
} else {
(content ?? componentView.animator).insert(componentView: componentView, view: view, frame: frame)
(content ?? hostingView.componentEngine.animator).insert(hostingView: hostingView, view: view, frame: frame)
}
}

public func delete(componentView: ComponentDisplayableView, view: UIView, completion: @escaping () -> Void) {
public func delete(hostingView: UIView, view: UIView, completion: @escaping () -> Void) {
if let deleteBlock {
deleteBlock(componentView, view, completion)
deleteBlock(hostingView, view, completion)
} else {
(content ?? componentView.animator).delete(componentView: componentView, view: view, completion: completion)
(content ?? hostingView.componentEngine.animator).delete(hostingView: hostingView, view: view, completion: completion)
}
}
}
31 changes: 0 additions & 31 deletions Sources/UIComponent/Components/View/ComponentViewComponent.swift

This file was deleted.

23 changes: 2 additions & 21 deletions Sources/UIComponent/Components/View/PrimaryMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ public class PrimaryMenu: UIControl {
set { config = newValue }
}

/// The view that displays the menu's content.
let contentView = ComponentView()

/// A flag indicating whether the menu is currently being displayed.
public var isShowingMenu = false

Expand Down Expand Up @@ -81,12 +78,6 @@ public class PrimaryMenu: UIControl {
set { _preferredMenuElementOrder = newValue }
}

/// The component that is rendered within the `contentView`.
public var component: (any Component)? {
get { contentView.component }
set { contentView.component = newValue }
}

/// A type-erased pointer style provider.
private var _pointerStyleProvider: Any?

Expand All @@ -110,24 +101,14 @@ public class PrimaryMenu: UIControl {
super.init(frame: frame)
showsMenuAsPrimaryAction = true
isContextMenuInteractionEnabled = true
addSubview(contentView)
accessibilityTraits = .button
contentView.addInteraction(UIPointerInteraction(delegate: self))
addInteraction(UIPointerInteraction(delegate: self))
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

public override func layoutSubviews() {
super.layoutSubviews()
contentView.frame = bounds
}

public override func sizeThatFits(_ size: CGSize) -> CGSize {
contentView.sizeThatFits(size)
}

// MARK: - Touch Handling

public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
Expand Down Expand Up @@ -177,7 +158,7 @@ extension PrimaryMenu: UIPointerInteractionDelegate {
if let pointerStyleProvider {
return pointerStyleProvider()
} else {
return UIPointerStyle(effect: .automatic(UITargetedPreview(view: contentView)), shape: nil)
return UIPointerStyle(effect: .automatic(UITargetedPreview(view: self)), shape: nil)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public struct PrimaryMenuRenderNode: RenderNode {
public func updateView(_ view: PrimaryMenu) {
view.config = config
view.menuBuilder = menuBuilder
view.contentView.engine.reloadWithExisting(component: component, renderNode: content)
view.componentEngine.reloadWithExisting(component: component, renderNode: content)
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/UIComponent/Components/View/TappableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct TappableViewConfig {
@available(*, deprecated, renamed: "TappableViewConfig")
public typealias TappableViewConfiguration = TappableViewConfig

/// TappableView is a subclass of ``ComponentView`` that responds to tap and gesture events.
/// TappableView is view that respond to tap and gesture events.
/// It can be configured using ``TappableViewConfig`` and supports various gestures such as tap, double tap, and long press.
///
/// ### Handle Gesture
Expand Down Expand Up @@ -68,7 +68,7 @@ public typealias TappableViewConfiguration = TappableViewConfig
/// }.pointerStyleProvider {
/// UIPointerStyle(...) // return the pointer style you want to be displayed
/// }
open class TappableView: ComponentView {
open class TappableView: UIView {
/// The configuration object for the TappableView, which defines the behavior of the view when it is tapped or highlighted.
public var config: TappableViewConfig?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public struct TappableViewRenderNode: RenderNode {
public func updateView(_ view: TappableView) {
view.config = config
view.onTap = onTap
view.engine.reloadWithExisting(component: component, renderNode: content)
view.componentEngine.reloadWithExisting(component: component, renderNode: content)
}
}

Expand Down
Loading

0 comments on commit 6bd9624

Please sign in to comment.