Releases: MihaelIsaev/UIKitPlus
🎉Release 2.0.0
All the changes are visible in the readme, new project template, and in updated examples! Enjoy! 🚀
Relative constraints with tags 🔥
Really often we have to create some views with constraints related to each other 😃
Before
Before that release we had to create a variable with view somewhere outside, like this
let someView = UView()
then we used it with other views to make relative constraints
UView {
someView.size(200).background(.red).centerInSuperview()
UView().size(100).background(.cyan).centerXInSuperview().top(to: someView)
UView().size(100).background(.purple).centerXInSuperview().bottom(to: someView)
UView().size(100).background(.yellow).centerYInSuperview().right(to: someView)
UView().size(100).background(.green).centerYInSuperview().left(to: someView)
}
Now
But since now we can easily rely just to view's tag
UView {
UView().size(200).background(.red).centerInSuperview().tag(7)
UView().size(100).background(.cyan).centerXInSuperview().top(to: 7)
UView().size(100).background(.purple).centerXInSuperview().bottom(to: 7)
UView().size(100).background(.yellow).centerYInSuperview().right(to: 7)
UView().size(100).background(.green).centerYInSuperview().left(to: 7)
}
Even order doesn't matter 🤗
UView {
UView().size(100).background(.cyan).centerXInSuperview().top(to: 7)
UView().size(100).background(.purple).centerXInSuperview().bottom(to: 7)
UView().size(100).background(.yellow).centerYInSuperview().right(to: 7)
UView().size(100).background(.green).centerYInSuperview().left(to: 7)
UView().size(200).background(.red).centerInSuperview().tag(7)
}
You even can add view later and all related views will immediately stick to it once it's added 🚀
let v = UView {
UView().size(100).background(.cyan).centerXInSuperview().top(to: 7)
UView().size(100).background(.purple).centerXInSuperview().bottom(to: 7)
UView().size(100).background(.yellow).centerYInSuperview().right(to: 7)
UView().size(100).background(.green).centerYInSuperview().left(to: 7)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
UIView.animate(withDuration: 1) {
v.body {
UView().size(200).background(.red).centerInSuperview().tag(7)
}
}
}
Gestures 🎉
Tested, Fixed and Improved all the gestures
We support all available gestures
TapGestureRecognizer
TapGestureRecognizer(taps: 1, touches: 1)
or
TapGestureRecognizer()
.numberOfTapsRequired(1)
.numberOfTouchesRequired(1)
💡
numberOfTapsRequired
andnumberOfTouchesRequired
also accept@State
also you can conveniently call onTapGesture
at any view
.onTapGesture {}
.onTapGesture { view in }
.onTapGesture { view, state in }
.onTapGesture { view, state, recognizer in }
.onTapGesture(taps: 1, touches: 1) { view, state in }
.onTapGesture(taps: 1, touches: 1) { view, state, recognizer in }
.onTapGesture(taps: 1, touches: 1, $someState)
.onTapGesture(taps: 1, touches: 1, on: .ended) { }
.onTapGesture(taps: 1, touches: 1, on: .ended) { view in }
LongPressGestureRecognizer
LongPressGestureRecognizer(taps: 0, touches: 1, minDuration: nil, allowableMovement: nil)
or
LongPressGestureRecognizer(taps: 0, touches: 1)
.minimumPressDuration(500)
.allowableMovement(10)
💡
minimumPressDuration
andallowableMovement
also accept@State
also you can conveniently call onLongPressGesture
at any view
.onLongPressGesture { view in }
.onLongPressGesture { view, state in }
.onLongPressGesture { view, state, recognizer in }
.onLongPressGesture(taps: 1, touches: 1) { view, state in }
.onLongPressGesture(taps: 1, touches: 1) { view, state, recognizer in }
.onLongPressGesture(taps: 1, touches: 1, $someState)
.onLongPressGesture(taps: 1, touches: 1, on: .ended) { }
.onLongPressGesture(taps: 1, touches: 1, on: .ended) { view in }
PinchGestureRecognizer
PinchGestureRecognizer(scale: 10)
or
PinchGestureRecognizer().scale(10)
💡
scale
also accept@State
also you can conveniently call onPinchGesture
at any view
// there are a lot of convenient combinations of this method
.onPinchGesture { view, state, recognizer in
print("changed scale: \(recognizer.scale) velocity: \(recognizer.velocity)")
view.size(200 * r.scale)
}
RotationGestureRecognizer
RotationGestureRecognizer(rotation: 2)
or
RotationGestureRecognizer().rotation(2)
💡
rotation
also accept@State
also you can conveniently call onRotationGesture
at any view
// there are a lot of convenient combinations of this method
.onRotationGesture { view, state, recognizer in
if state == .changed {
view.transform = CGAffineTransform(rotationAngle: recognizer.rotation)
}
}
SwipeGestureRecognizer
SwipeGestureRecognizer(direction: .left, touches: nil)
or
SwipeGestureRecognizer(direction: .left)
.numberOfTouchesRequired(1)
💡
numberOfTouchesRequired
also accept@State
also you can conveniently call onSwipeGesture
at any view
// there are a lot of convenient combinations of this method
.onSwipeGesture(direction: .left) { view, state, recognizer in }
PanGestureRecognizer
PanGestureRecognizer(minTouches: 1, maxTouches: 2)
or
PanGestureRecognizer()
.minimumNumberOfTouches(1)
.maximumNumberOfTouches(1)
💡
minimumNumberOfTouches
andmaximumNumberOfTouches
also accept@State
also you can conveniently call onPanGesture
at any view
// there are a lot of convenient combinations of this method
.onPanGesture { view, state, recognizer in }
ScreenEdgePanGestureRecognizer
ScreenEdgePanGestureRecognizer(edges: .all)
and additional .edges
method gives you an ability to change edges property later
.edges(.left)
💡
edges
also accept@State
also you can conveniently call onScreenEdgePanGesture
at any view
// there are a lot of convenient combinations of this method
.onScreenEdgePanGesture(edges: .left) { view, state, recognizer in }
HoverGestureRecognizer
HoverGestureRecognizer()
you can conveniently call onHoverGesture
or hovered
at any view
.hovered {
print("hovered: \($0)")
}
or
.onHoverGesture { view, state, recognizer in
switch state {
case .began:
print("began")
view.backgroundColor = .magenta
case .changed:
print("changed")
view.backgroundColor = .orange
case .ended:
print("ended")
view.backgroundColor = .green
default: print("default")
}
}
State tracking
Any gesture recognizer has tracking methods
There are universal trackState
method which contains state
and optionally recognizer
.trackState { state, recognizer in
// do switch/case here
}
💡
trackState
can accept@State
And additionally you have an ability to listen for exact state easily 👍
.onPossible { recognizer in }
.onBegan { recognizer in }
.onChanged { recognizer in }
.onEnded { recognizer in }
.onCancelled { recognizer in }
.onFailed { recognizer in }
💡
recognizer
is optional
Delegate
Any gesture recognizer has its delegate methods available directly
You can set delegate simply like this
.delegate(...)
Additionally you can use these convenient methods below and even combine them with the classic delegate
.shouldBegin { recognizer in
return true
}
.shouldReceivePress { recognizer, press in
return true
}
.shouldReceiveTouch { recognizer, touch in
return true
}
.shouldRequireFailureOfOtherGestureRecognizer { currentRecognizer, otherRecognizer in
return false
}
.shouldBeRequiredToFailByOtherGestureRecognizer { currentRecognizer, otherRecognizer in
return false
}
.shouldRecognizeSimultaneouslyWithOtherGestureRecognizer { currentRecognizer, otherRecognizer in
return true
}
How to add a gesture to view
UView().gesture(myGesture)
or
UView().gestures(myGesture1, myGesture2)
or even
UView().gestures {
myGesture1
myGesture2
// unlimited amount
}
Multiple gestures at the same time (simple example for pinch + rotate)
UView().size(200).background(.red).centerInSuperview().gestures { v in
PinchGestureRecognizer().shouldRecognizeSimultaneouslyWithOtherGestureRecognizer { og in
print("PinchGestureRecognizer shouldRecognizeSimultaneouslyWith: \(og.tag)")
return true
}.trackState { s, r in
if s == .changed {
v.size(200 * r.scale)
}
}.tag(3)
RotationGestureRecognizer().shouldRecognizeSimultaneouslyWithOtherGestureRecognizer { og in
print("PinchGestureRecognizer shouldRecognizeSimultaneouslyWith: \(og.tag)")
return true
}.trackState { s, r in
if s == .changed {
v.transform = CGAffineTransform(rotationAngle: r.rotation)
}
}.tag(4)
}
View's touch methods which available without gesture recognizers
.touchesBegan { view, touch, event in }
.touchesMoved { view, touch, event in }
.touchesEnded { view, touch, event in }
.touchesCancelled { view, touch, event in }
💡 All the methods above has its convenient shorter variations
Declarative ViewController
Sometimes you may need to declare view controller quickly without creating a separate file for it
ViewController {
UView()
UView()
UView()
// add any amount of subviews
}
.background(.gray)
.statusBarStyle(.dark)
.title("Hello world")
.navigationController { nav in }
.navigationItem { navItem in }
.onViewDidLoad { vc in }
.onViewDidLayoutSubviews { vc in }
.onViewWillAppear { vc, animated in }
.onViewWillAppearFirstTime { vc, animated in }
.onViewDidAppear { vc, animated in }
.onViewDidAppearFirstTime { vc, animated in }
.onViewWillDisappear { vc, animated in }
.onViewDidDisappear { vc, animated in }
.touchesBegan { vc, touches, event in }
.touchesMoved { vc, touches, event in }
.touchesCancelled { vc, touches, event in }
.touchesEnded { vc, touches, event in }
Preview group 🔥
It is convenient way to create multiple previews inside one struct
Limitations:
- only 10 previews inside group
rtl
andlanguage
properties can be set only to group, not to previews directly
#if canImport(SwiftUI)
import SwiftUI
@available(iOS 13.0, *)
struct MyPreviewGroup_Preview: PreviewProvider, DeclarativePreviewGroup {
static var previewGroup: PreviewGroup {
PreviewGroup { // 1 to 10 previews inside
Preview {
MainViewController()
}
.colorScheme(.dark)
.device(.iPhoneX)
Preview {
MainViewController()
}
.colorScheme(.light)
.device(.iPhoneX)
Preview {
// in this group title will be shown in `fr` language
UButton(String(.en("Hello"), .fr("Bonjour"), .ru("Привет")))
.circle()
.background(.blackHole / .white)
.color(.white / .black)
.height(54)
.edgesToSuperview(h: 8)
.centerYInSuperview()
}
.colorScheme(.dark)
.layout(.fixed(width: 300, height: 64))
}
.language(.fr) // limited to group
.rtl(true) // limited to group
}
}
#endif
Ready to beat SwiftUI
Hurray, I made UIKitPlus even more powerful than before! It is updated to Swift 5.2 and Xcode 11.4!
For those who doesn't know what is UIKitPlus
It is complex production-ready solution for easy building amazing UI on UIKit.
This is a UIKit-wrapper which makes is declarative, allows to add subviews in SwiftUI style, and gives the most beautiful way to declare constraints! So no more need inSnapKit
or its clones! Even more, constraints may be set in advance anywhere, so before you add view to superview! 🚀
You can start using it today even for one or two views. Just try it and you will never want to go back! 🤷
I refactored and updated EXAMPLE PROJECT so now LivePreview works in every file just like in SwiftUI!
In the library's readme you will find detailed examples for everything, bu here I want to highlight some awesome bonus features:
- easily declare dynamic colors for dark/light modes
let titleColor: UIColor = .black / .white // second color is for dark mode (works automatically ofc)
-
root view controller
This view controller you could use as enter point to the app and inside it declare routes to other view controllers and its transitions. It gives an ability to easily switch between controllers and support deep links.
Documentation is here -
localize your strings easily
String(.en("Hello"),
.fr("Bonjour"),
.ru("Привет"),
.es("Hola"),
.zh_Hans("你好"),
.ja("こんにちは"))
- build attributed strings easily
"Hello".foreground(.darkGrey).font(.articoRegular, 15)
- and even concatenate them easily
"Hello ".foreground(.green).font(.articoRegular, 15) + "world".foreground(.red).font(.articoRegular, 15)
- format textfields easily! example with AnyFormatKit lib
static var phone: TextField {
TextField()
.content(.telephoneNumber)
.autocapitalization(.none)
.autocorrection(.no)
.keyboard(.phonePad)
.font(.articoRegular, 15)
.placeholder("(555) 123-4567".foreground(.darkGrey).font(.articoRegular, 15))
.color(.blackHole)
.height(18)
.formatCharacters { textView, range, text in
// here we use `AnyFormatKit`
let phoneFormatter = DefaultTextInputFormatter(textPattern: "(###) ###-####")
let result = phoneFormatter.formatInput(currentText: textView.text ?? "", range: range, replacementString: text)
textView.text = result.formattedText
}
}
and even more cool, beautiful and convenience features ❤️ because I'm writing this lib for myself and just would love to share it with you 😃
If you will use it please don't hesitate to ask me any questions! I'm ready to help with anything!
You have a lot of ways to get help:
- file an issue here
- write me in Discord directly to
iMike#3049
- write me in Discord in #ios channel on Vapor Discord server
- write me in Discord in #uikitplus channel in Swift Stream Discord server
Implement TextView
@State var text = ""
TextView($text)
.tag(0)
.background(.purple)
.color(.white)
.tint(.red)
.placeholder("Placeholder title")
.corners(4)
.inputAccessoryView { SomeAccessoryView() }
.height(88)
Implement VSpace, HSpace, Space
VStack {
View1()
VSpace(16) // adds 16pt empty view
View2()
}
HStack {
View1()
HSpace(16) // adds 16pt empty view
View2()
}
HStack {
View1().width(100)
View2().width(32)
Space() // adds flexible empty view
}
Brand new VScrollStack and HScrollStack ❤️
Use VScrollStack
and HScrollStack
as simple as VStack
and HStack
but with scrolling!
It contains methods from both StackView
and ScrollView
.
body {
VScrollStack {
Text("Text on top")
View().height(400).color(.red)
Text("Text 400px from top")
View().height(400).color(.cyan)
Text("Text ~800px from top")
View().height(400).color(.purple)
Text("Text at the bottom")
}
.edgesToSuperview()
.spacing(16) // stackview method
.hideAllIndicators() // scrollview method
}
New portion of useful things and fixes
- DeclarativeProtocol: implement
tag
method - Image: implement more
url
methods - PickerView: implement
textColor
methods - Button: fix reactive title logic
- TextField: implement
shouldReturnToNextResponder
- ViewController: improve keyboard notification handlers by retrieving animation curve info to flawlessly show/hide keyboard with $keyboardHeight