Swiftpack.co - Package - MihaelIsaev/UIKitPlus

MIT License Swift 5.1 Cocoapod

This project is in active development state

With that lib you could build UI in SwiftUI-like way for iOS9 and higher! 😺

// NOTE:
// Now it is written for Swift 5.1 and Xcode 11
// stable code for Swift 4.2 is available in swift4 branch

Support this lib by giving a ⭐️!

KNOWN ISSUE

⚠️ don't use Xcode 11.2 because of compiler bug in it which is showing Type of expression is ambiguous without more context in function builders if expression is long. Use Xcode 11.1 cause there everything compiles fine. Hope this issue will be fixed in nearest Xcode update.

Xcode 11.1 available here https://developer.apple.com/download/more/

If you experience any compilation problems in Xcode 11.1 please go to Xcode -> Preferences -> Location open derived data folder in finder and remove everything from it manually. Then restart Xcode and compile your project without any errors.

Learn how to use through the Example project 🎮

Please feel free to request for specific examples in example project issues I'll try to cover as much as I can 🚀

Main Features

You can build your view anywhere with all the constraints (even to other views), and then once you add it into a superview all the constraints will be activated.

Reusing views is pretty easy. Just declare them in extensions!

Really short intro

class MyViewController: ViewController {
    lazy var view1 = View()
    lazy var view2 = View()

    override func buildUI() {
        super.buildUI()
        body {
            view1.background(.black).size(100).centerInSuperview()
            view2.background(.red).size(30, 20).centerXInSuperview().top(to: .bottom, of: view1, 16)
        }
    }
}

Long intro

import UIKit
import UIKitPlus

// Just feel how easy you could build & declare your views
// with all needed constraints, properties and actions
// even before adding them to superview!
class LoginViewController: ViewController {
    @State var email = ""
    @State var password = ""
    
    override func buildUI() {
        super.buildUI()
        view.backgroundColor = .black
        body {
            Button.back.onTapGesture { print("back tapped") }
            Label.welcome.text("Welcome").centerXInSuperview().topToSuperview(62)
            VStack {
                TextField.welcome.text($email).placeholder("Email").keyboard(.emailAddress).content(.emailAddress)
                TextField.welcome.text($password).placeholder("Password").content(.password).secure()
                View().height(10) // just to add extra space
                Button.bigBottomGreen.title("Sign In").tapAction(signIn)
            }.edgesToSuperview(top: 120, leading: 16, trailing: -16)
        }
    }

    func signIn() {
        // do an API call to your server with my awesome CodyFire lib 😉
    }
}

And that's it! Yeah! You just need few extensions to make it work 😍

// PRO-TIP:
// To avoid mess declare reusable views in extensions like this
extension FontIdentifier {
    static var sfProRegular = FontIdentifier("SFProDisplay-Regular")
    static var sfProMedium = FontIdentifier("SFProDisplay-Medium")
}
extension Text {
    static var title: Text { Text().color(.white).font(.sfProMedium, 18) }
}
extension TextField {
    static var welcome: TextField {
        TextField()
            .height(40)
            .background(.clear)
            .color(.black)
            .tint(.mainGreen)
            .border(.bottom, 1, .gray)
            .font(.sfProRegular, 16)
    }
}
extension Button {
    static var back: Button { return Button("backIcon").topToSuperview(64).leadingToSuperview(24) }
    static var bigBottomGreen: Button {
        Button()
            .color(.white)
            .font(.sfProMedium, 15)
            .background(.green)
            .height(50)
            .circle()
            .shadow(.gray, opacity: 1, offset: .init(width: 0, height: -1), radius: 10)
    }
}

// PRO-TIP2:
// I'd suggest you to use extensions for everything: fonts, images, labels, buttons, colors, etc.

Installation

With CocoaPods

Add the following line to your Podfile:

pod 'UIKit-Plus', '~> 1.8.0'

With Swift Package Manager

In Xcode11 go to File -> Swift Packages -> Add Package Dependency and enter there URL of this repo

https://github.com/MihaelIsaev/UIKitPlus

With Carthage

Not supported yet. Feel free to send PR for that.

Usage

import UIKit
import UIKitPlus

Cheatsheet

| | UIKitPlus | UIKit | | - | ------- | -------------- | | ✅ | View | UIView | | ✅ | WrapperView | UIView | | ✅ | ScrollView | UIScrollView | | ✅ | CollectionView, Collection | UICollectionView | | ✅ | TableView, List, StaticList | UITableView | | ✅ | Image | UIImageView | | ✅ | Button | UIButton | | ✅ | Text | UILabel | | ✅ | TextField | UITextField | | ✅ | SegmentedControl | UISegmentedControl | | ✅ | VisualEffectView | UIVisualEffectView | | ✅ | StackView | UIStackView | | ✅ | HStack | UIStackView | | ✅ | VStack | UIStackView | | ✅ | VerificationCodeView | | | ✅ | AttributedString (aka AttrStr) | NSAttributedString | | ✅ | ViewController | UIViewController | | ✅ | NavigationController | UINavigationController | | ✅ | FormController | | | ✅ | DatePicker | UIDatePicker | | ✅ | Stepper | UIStepper | | ✅ | Slider | UISlider | | ✅ | Toggle | UISwitch |

✋ Readme below should be updated, cause there are a lot of new features in v1.0.0 and higher

View

It is just a simple view with ability to customize it declarative way

View().background(.red).shadow().edgesToSuperview()

Also you can initialize it with predefined subviews

View.subviews {
    let avatar = Image("some").size(100)
//                             stick to top, leading and trailing of superview
                              .edgesToSuperview(top: 0, leading: 0, trailing: 0)

//                                 stick top to bottom of avatar view with 16pt
    let name = Label("John Smith").top(to: .bottom, of: avatar, 8)
//                                 stick to leading, trailing and bottom of superview
                                  .edgesToSuperview(leading: 0, trailing: 0, bottom: 0)
    return [avatar, name]
}

WrapperView

It is simple View but with ability to initialize with inner view

WrapperView {
  View().background(.red).shadow()
}.background(.green).shadow()

and you could specify innerView`s padding right here

// to the same padding for all sides
WrapperView {
  View()
}.padding(10)
// or to specific padding for each side
WrapperView {
  View()
}.padding(top: 10, left: 5, right: 10, bottom: 5)
// or even like this
WrapperView {
  View()
}.padding(top: 10, right: 10)

ScrollView

Nothing interesting yet, but you could specify some settings in declarative manner

ScrollView().paging(true).scrolling(false).hideIndicator(.horizontal)
ScrollView().paging(true).scrolling(false).hideAllIndicators()
ScrollView().contentInset(.zero)
ScrollView().contentInset(top: 10, left: 5, right: 5, bottom: 10)
ScrollView().contentInset(top: 10, bottom: 10)
ScrollView().scrollIndicatorInsets(.zero)
ScrollView().scrollIndicatorInsets(top: 10, left: 5, right: 5, bottom: 10)
ScrollView().scrollIndicatorInsets(top: 10, bottom: 10)

CollectionView

Nothing interesting yet

CollectionView().paging(true)

TableView

Nothing interesting yet

Image

Image("someImage").mode(.scaleAspectFill).clipsToBounds(true)

Button

Button()
Button("Tap me")
Button().title("Tap me") // useful if you declared Button from extension like below
Button.mySuperButton.title("Tap me")

background and background for highlighted state

Button("Tap me").background(.white).backgroundHighlighted(.darkGray)

title color for different states

Button("Tap me").color(.black).color(.lightGray, .disabled)

set some font from declared identifiers or with system fonts

Button("Tap me").font(v: .systemFont(ofSize: 15))
Button("Tap me").font(.sfProBold, 15)

add image

Button("Tap me").image(UIImage(named: "cat"))
Button("Tap me").image("cat")

You can handle tap action easily

Button("Tap me").tapAction { print("button tapped") }
Button("Tap me").tapAction { button in
    print("button tapped")
}

or like this

func tapped() { print("button tapped") }
Button("Tap me").tapAction(tapped)

func tapped(_ button: Button) { print("button tapped") }
Button("Tap me").tapAction(tapped)

Label

It either may be initialized with String or unlimited amount of AttributedStrings

Label("hello 👋 ")
Label().text("hello") // useful if declare label in extension like below
Label.mySuperLabel.text("hello")
Label(AttributedString("hello").foreground(.red), AttributedString("world").foreground(.green))

set some font from declared identifiers or with system fonts

Label("hello").font(v: .systemFont(ofSize: 15))
Label("hello").font(.sfProBold, 15)

set text color

Label("hello").color(.red)

set text alignment

Label("hello").alignment(.center)

set amount of lines

Label("hello").lines(1)
Label("hello\nworld").lines(0)
Label("hello\nworld").lines(2)
Label("hello\nworld").multiline()

TextField

TextField()
TextField("some text")
TextField().text("some text")
TextField.mySuperDuperTextField.text("some text")

set some font from declared identifiers or with system fonts

TextField().font(v: .systemFont(ofSize: 15))
TextField().font(.sfProBold, 15)

set text color

TextField().color(.red)

set text alignment

TextField().alignment(.center)

placeholder

TextField().placeholder("email")
// or use AttributedString to make it colored
TextField().placeholder(AttributedString("email").foreground(.green))

secure

TextField().secure()

remove any text from field easily

TextField().cleanup()

set keyboard and content type

TextField().keyboard(.emailAddress).content(.emailAddress)

set delegate

TextField().delegate(self)

or get needed events declarative way

TextField().shouldBeginEditing { tf in return true }
           .didBeginEditing { tf in }
           .shouldEndEditing { tf in return true }
           .didEndEditing { tf in }
           .shouldChangeCharacters { tf, range, replacement in return true }
           .shouldClear { tf in return true }
           .shouldReturn { tf in return true }
           .editingDidBegin { tf in }
           .editingChanged { tf in }
           .editingDidEnd { tf in }

SegmentedControl

SegmentedControl("One", "Two").select(1).changed { print("segment changed to \($0)") }

VisualEffectView

VisualEffectView(.darkBlur)
VisualEffectView(.lightBlur)
VisualEffectView(.extraLightBlur)
// iOS10+
VisualEffectView(.prominent)
VisualEffectView(.regular)

Create your own extension for your custom effects to use them easily like in example above

extension UIVisualEffect {
    public static var darkBlur: UIVisualEffect { return UIBlurEffect(style: .dark) }
}

StackView

StackView().axis(.vertical)
           .alignment(.fill)
           .distribution(.fillEqually)
           .spacing(16)

HStackView

The same as StackView but with predefined axis and ability to easily add arranged subviews

HStackView (
  Label("hello world").background(.green),
  Label("hello world").background(.red)
).spacing(10)

VStackView

The same as StackView but with predefined axis and ability to easily add arranged subviews

VStackView (
  Label("hello world").background(.green),
  Label("hello world").background(.red)
).spacing(10)

VerificationCodeView

This is really bonus view! :D Almost every app now uses verification codes for login and now you can easily implement that code view with UIKitPlus! :)

VerificationCodeField().digitWidth(64)
                       .digitsMargin(25)
                       .digitBorder(.bottom, 1, 0xC6CBD3)
                       .digitColor(0x171A1D)
                       .font(.sfProRegular, 32)
                       .entered(verify)

func verify(_ code: String) {
  print("entered code: " + code)
}

Any view

Background

View().background(.red)
View().background(0xff0000)

Tint

View().tint(.red)
View().tint(0xff0000)

Corners

To set radius to all corners

View().corners(10)

To set custom radius for specific corner

View().corners(10, .topLeft, topRight)
View().corners(10, .topLeft, .bottomRight)
View().corners(10, .topLeft, topRight, .bottomLeft, .bottomRight)

To make you view's corners round automatically by smaller side

View().circle()

Borders

To set border on all sides

View().border(1, .black)
View().border(1, 0x000)

To set border on specific side

View().border(.top, 1, .black)
View().border(.left, 1, .black)
View().border(.right, 1, .black)
View().border(.bottom, 1, .black)

To remove border from specific side

.removeBorder(.top)

Alpha

View().alpha(0)

Opacity

View().opacity(0)

Hidden

View().hidden() // true by default
View().hidden(true)
View().hidden(false)

Rasterize

To rasterize layer, e.g. for better shadow performance

View().rasterize() // true by default
View().rasterize(true)
View().rasterize(false)

Shadow

View().shadow() // by default it's black, opacity 1, zero offset, radius 10
View().shadow(.gray, opacity: 0.8, offset: .zero, radius: 5)
View().shadow(0x000000, opacity: 0.8, offset: .zero, radius: 5)

Shake

You can shake any view just by calling

View().shake()

And you could customize shake effect

View().shake(values: [-20, 20, -20, 20, -10, 10, -5, 5, 0],
             duration: 0.6,
             axis: .horizontal,
             timing: .easeInEaseOut)
View().shake(-20, 20, -20, 20, -10, 10, -5, 5, 0,
             duration: 0.6,
             axis: .horizontal,
             timing: .easeInEaseOut)

or even create an extension

import UIKitPlus

extension DeclarativeProtocol {
  func myShake() {
      View().shake(-20, 20, -20, 20, -10, 10, -5, 5, 0,
                   duration: 0.6,
                   axis: .horizontal,
                   timing: .easeInEaseOut)
  }
}

AttributedString

You could create attributed strings in declarative way easily

AttributedString("hello").background(.gray)
                         .foreground(.red)
                         .font(.sfProBold, 15)
                         .paragraphStyle(.default)
                         .ligature(1)
                         .kern(1)
                         .strikethroughStyle(1)
                         .underlineStyle(.patternDash)
                         .strokeColor(.purple)
                         .strokeWidth(1)
                         .shadow()
                   // or .shadow(offset: .zero, blur: 1, color: .lightGray)
                         .textEffect("someEffect")
                         .attachment(someAttachment)
                         .link("http://github.com")
                         .baselineOffset(1)
                         .underlineColor(.cyan)
                         .strikethroughColor(.magenta)
                         .obliqueness(1)
                         .expansion(1)
                         .glyphForm(.horizontal)
                         .writingDirection(.rightToLeft)

Constraints

Size

You can set view size by passing width and height values like this

View().size(100, 200)

or square size by passing single value

View().size(100)

or view's size can be equal to other view size so when you change size of one view other view will change its size as well

let view1 = View().size(100, 200)
let view2 = View().equalSize(to: view1)

Of course you can specify just width or just height or both but by separate methods

View().width(100)
View().height(200)
View().width(100).height(200)
Superview

Your view can stick to its superview by any side or even by all sides

// this way it will stick with 0 constant value
View().edgesToSuperview()
// this way it will stick with 10 constant value for all sides
View().edgesToSuperview(10)
// also you could specify some values manually, but all the rest values are 0 by default
View().edgesToSuperview(top: 16, leading: 16, trailing: -16)
View().edgesToSuperview(trailing: -16, bottom: -16)

you could stick your view to only one side of superview like this

// empty argument means 0 constant value
View()topToSuperview()
View()topToSuperview(16)
View()leadingToSuperview()
View()leadingToSuperview(16)
View()trailingToSuperview()
View()trailingToSuperview(16)
View()bottomToSuperview()
View()bottomToSuperview(16)
View()centerXToSuperview()
View()centerXToSuperview(16)
View()centerYToSuperview()
View()centerYToSuperview(16)
View()widthToSuperview()
View()heightToSuperview()
Relative

Any side of your view could also stick to any side of other view

// Sides to superview
View().top(to: .bottom, of: someView, 16) // stick view's top to someView`s bottom with 16pt (by default 0pt)
View().leading(to: .trailing, of: someView)
View().trailing(to: .leading, of: someView)
View().bottom(to: .top, of: someView)
// Center to superview
View().centerX(to: .centerX, of: someView)
View().centerY(to: .centerY, of: someView)
// Dimension Superview
View().width(to: .width, of: someView)
View().height(to: .height, of: someView)

or this way

// Sides to superview
View().edge(.top, toSuperview: someView, .bottom)
View().edge(.leading, toSuperview: someView, .trailing)
View().edge(.trailing, toSuperview: someView, .leading)
View().edge(.bottom, toSuperview: someView, .top)
// Center to superview
View().edge(.centerX, toSuperview: someView, .centerX)
View().edge(.centerY, toSuperview: someView, .centerY)
// Dimension Superview
View().edge(.width, toSuperview: someView, .width)
View().edge(.height, toSuperview: someView, .height)

To build constraints between two relative views

// Sides to another views
View().spacing(.leading, to: relativeView, toSide: .trailing, 16) // last parameter is optional, 0 by default
View().spacing(.trailing, to: relativeView, toSide: .leading)
View().spacing(.top, to: relativeView, toSide: .bottom)
View().spacing(.bottom, to: relativeView, toSide: .top)
// Center to another relative views
View().center(.x, to: relativeView, toSide: .x)
View().center(.y, to: relativeView, toSide: .y)
// Dimension Relative
View().dimension(.width, to: relativeView, toSide: .width)
View().dimension(.height, to: relativeView, toSide: .height)
Center

Your view could be in center of its superview

View().centerInSuperview() // exact center
View().centerInSuperview(10) // exact center +10 by x-axis, and +10 by y-axis
View().centerInSuperview(x: 5, y: 10) // exact center +5 by x-axis, and +10 by y-axis

also it may be in center of another view

View().center(to: anotherView) // exact center
View().center(to: anotherView, 10) // exact center +10 by x-axis, and +10 by y-axis
View().center(to: anotherView, x: 5, y: 10) // exact center +5 by x-axis, and +10 by y-axis
Constraints direct access

Ok, let's imagine that you have a view which is sticked to its superview

let view = View().edgesToSuperview()

now your view have top, leading, trailing and bottom constraints to its superview and e.g. you want to change top constraint so you could do it like this

view.top = 16

or

view.declarativeConstraints.top?.constant = 16

the same way works with all view's constraints, so you can change them or even delete them just by setting them nil.

Another situation if you have a view which have a constrain to another relative view

let centerView = View().background(.black).size(100).centerInSuperview()
let secondView = View().background(.green).size(100).centerXInSuperview().top(to: .bottom, of: centerView, 16)

and for example you want to reach bottom constraint of centerView related to secondView, do it like this

// short way
centerView.outer[.bottom, secondView] = 32 // changes their vertical spacing from 16 to 32
// long way
centerView.declarativeConstraints.outer[.bottom, secondView]?.constant = 32 // changes their vertical spacing from 16 to 32
Layout Margin
// to all sides
View().layoutMargin(10)
// optional sides
View().layoutMargin(top: 10)
View().layoutMargin(left: 10, bottom: 5)
View().layoutMargin(top: 10, right: 5)
// vertical and horizontal
View().layoutMargin(x: 10, y: 5) // top: 5, left: 10, right: 10, bottom: 5
View().layoutMargin(x: 10) // left: 10, right: 10
View().layoutMargin(y: 5) // top: 5, bottom: 5
SafeArea

You could get safeArea safely at any view without #available(iOS 11.0, *) check like this

someView.safeArea.topAnchor
Constraint values

Any constraint value may be set as CGFloat or with Relation and even Multiplier

// just equal to 10
View().leading(to: .trailing, of: anotherView, 10)

// greaterThanOrEqual to 10
View().leading(to: .trailing, of: anotherView, >=10)

// lessThanOrEqual to 10
View().leading(to: .trailing, of: anotherView, <=10)

// equal to 10 with 1.5 multiplier
View().leading(to: .trailing, of: anotherView, 10 ~ 1.5)

// equal to 10 with 1.5 multiplier and 999 priority
View().leading(to: .trailing, of: anotherView, 10 ~ 1.5 ! 999)

// equal to 10 with 1.5 multiplier and `.defaultLow` priority
View().leading(to: .trailing, of: anotherView, 10 ~ 1.5 ! .defaultLow)

// equal to 10 with 999 priority
View().leading(to: .trailing, of: anotherView, 10 ! 999)

Colors

With UIKitPlus you're able to use hex colors just by typing them with 0x prefix like 0x000 for black or 0xfff for white.

Extensions

Fonts

Add your custom fonts to the project and then declare them like this

import UIKitPlus

extension FontIdentifier {
    public static var sfProBold = FontIdentifier("SFProDisplay-Bold")
    public static var sfProRegular = FontIdentifier("SFProDisplay-Regular")
    public static var sfProMedium = FontIdentifier("SFProDisplay-Medium")
}

and then use them just like

Button().font(.sfProMedium, 15)

Colors

Declare custom colors like this

import UIKit
import UIKitPlus

extension UIColor {
    static var mainBlack: UIColor { return .black  }
    static var otherGreen: UIColor { return 0x3D7227.color  } // 61 114 39
}

and then use them just like

Label("Hello world").color(.otherGreen).background(.mainBlack)

Buttons

Declare custom buttons like this

import UIKitPlus

extension Button {
    static var bigBottomWhite: Button {
        return Button().color(.darkGray).color(.black, .highlighted).font(.sfProMedium, 15).background(.white).backgroundHighlighted(.lightGray).circle()
    }
    static var bigBottomGreen: Button {
        return Button().color(.white).font(.sfProMedium, 15).background(.mainGreen).circle()
    }
}

and then use them like this

Button.bigBottomWhite.size(300, 50).bottomToSuperview(20).centerInSuperview()

Labels

Declare custom attributed labels like this

import UIKitPlus

extension Label {
    static var welcomeLogo: Label {
        return .init(AttributedString("My").foreground(.white).font(.sfProBold, 26),
                     AttributedString("App").font(.sfProBold, 26))
    }
}

and then use them like this

let logo = Label.welcomeLogo.centerInSuperview()

Images

Declare asset images like this

import UIKitPlus

extension Image {
    static var welcomeBackground: Image { return Image("WelcomeBackground") }
}

and then use them like this

let backgroudImage = Image.welcomeBackground.edgesToSuperview()

Subviews

Add subviews easily

view.body {
    view1
    view2
    view3
}

Github

link
Stars: 56
Help us keep the lights on

Dependencies

Used By

Total: 0

Releases

1.8.0 - Nov 10, 2019

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
}

1.7.0 - Nov 7, 2019

  • 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

1.6.0 - Nov 7, 2019

RootController: implement attach(to scene:)

now in SceneDelegate file you could attach RootController like this

@available(iOS 13.0, *)
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        RootViewController().attach(to: scene) // it returns Window and you could store it if you want to
    }
}

without SceneDelegate you could attach RootController in AppDelegate like this

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    var rootViewController: RootViewController { window!.rootViewController as! RootViewController }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        RootViewController().attach(to: window)
        return true
    }
}

1.5.0 - Oct 30, 2019

Let's imagine that you already have MyViewController in your project

create e.g. Live.swift, paste this code, enable canvas Cmd+Opt+Enter and pin this canvas

#if canImport(SwiftUI) && DEBUG
import SwiftUI
import UIKitPlus
@available(iOS 13.0, *)
struct DemoView_Preview: PreviewProvider {
    static var previews: some SwiftUI.View {
        MyViewController()
            .liveView
            .previewDevice(PreviewDevice(rawValue: "iPhone X"))
            .edgesIgnoringSafeArea(.vertical)
    }
}
#endif

go to MyViewController and enjoy with live preview!

Then just replace MyViewController() to somethings else to switch to preview for another view controller or view.

1.0.0-alpha.1.0.0 - Sep 29, 2019

This release still support iOS9+ but it works with Swift 5.1+ only because of function builders and property wrappers 🚀 Readme will be updated soon! Stay tuned! P.S. If you can't wait write me a message on discord or take a look at commits.