Skip to content
This repository has been archived by the owner on Jan 14, 2021. It is now read-only.

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
  • Loading branch information
bok- committed Apr 20, 2018
1 parent b90fe27 commit 8409b1d
Show file tree
Hide file tree
Showing 14 changed files with 1,593 additions and 43 deletions.
34 changes: 34 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## Build generated
build/
DerivedData/

## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/

## Other
.DS_Store
*.moved-aside
*.xccheckout
*.xcscmblueprint

## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace
19 changes: 19 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (c) 2018 Unsigned Apps Pty Ltd

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
247 changes: 247 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
# Salon

Salon provides conditionally-applied, composible, and type-safe UIView styles using functions.

This framework is heavily inspired by [this post](https://medium.cobeisfresh.com/composable-type-safe-uiview-styling-with-swift-functions-8be417da947f) by [Marin Benčević](https://medium.cobeisfresh.com/@marinbenc). All credit should go to him.

## Compatibility

Salon is written and tested for Swift 4.0 and above.

## Installation

Installation is via Carthage at the moment. A CocoaPod may be published later, and you can't use Swift Package Manager with iOS apps yet so no point there.

You can also clone the repo and import it to your project manually - its not very big.

### Carthage

Salon is [Carthage](https://github.com/Carthage/Carthage) compatible. Just add it as a dependency in your Cartfile:

```ruby
github "unsignedapps/Salon"
```

## Basic Usage

Salon is designed to try and separate how you set style-related properties on your UIView (and related) objects from your normal code.

(If you haven't read [this post](https://medium.cobeisfresh.com/composable-type-safe-uiview-styling-with-swift-functions-8be417da947f) by [Marin Benčević](https://medium.cobeisfresh.com/@marinbenc), that might be a good place to start).

Here is a basic example of it in use:

```swift
let label = UILabel()
label.apply { label in
label.textColor = .red
label.font = .systemFont(ofSize: 14)
}
```

Note that you can basically do anything inside your style closure, so it is really powerful - but it can also be easily abused. This framework is designed to help separate view styling code from your regular code, if you choose to abuse that the potential mess that follows is entirely your responsibility 🙂.

So we've compartmentalised the style code into its own function, but its not very separate. Fortunately it is wrapped into a `ViewStyle` struct, so you can take it anywhere:

```swift
let style = ViewStyle<UILabel> { label in
label.textColor = .red
label.font = .systemFont(ofSize: 14)
}

let label = UILabel()
label.apply(style: style)
```

So that means you can separate it out into its own file and maintain them separately:

**MyStyles.swift:**
```swift
extension ViewStyle {

// Nice easy to use static var
static var myNiceRedLabel: ViewStyle<MyNiceLabel> {
return ViewStyle<MyNiceLabel> { label in
label.textColor = .red
label.font = .systemFont(ofSize: 14)
}
}

// You can also use the `ViewStyle.style()` static functions to infer the type and
// save typing ViewStyle<Type> multiple times.
static var myNicerRedLabel: ViewStyle<MyNiceLabel> {
return .style { label in
label.textColor = .red
label.font = .systemFont(ofSize: 14)
}
}
}
```

**MyNiceLabel.swift:**
```swift
final class MyNiceLabel: UILabel {
override init (frame: CGRect) {
super.init(frame: frame)
self.apply(style: .myNiceRedLabel)
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.apply(style: .myNiceRedLabel)
}
}
```

## Conditional Styles

Salon also supports conditional styles - styles that you might only apply under certain circumstances. For example, when a control is selected or highlighted, or when you working on a universal app and you want to target different horizontal and vertical size classes.

```swift
let style = ViewStyle<UILabel>(when: .horizontalSizeClass(equals: .regular)) { label in
label.textColor = .red
label.font = .systemFont(ofSize: 14)
}

let label = UILabel()
label.apply(style: style)
```

Or more directly:

```swift
let button = UIButton()
button.apply(when: .controlState(equals: .disabled)) { button in
button.backgroundColor = .black
}
```

(Note: Its not the intention of this framework to replace your usage of the `UIControl.setTitleColor(_:for:)` methods either - they will be updated automatically as the state changes, these styles won't.)

### Creating Conditions

A full list of the provided conditions can be found in `Conditions.swift`, but it basically covers all of the different `UITraitCollection` options, `UIControlState` and computed states for `UICollectionViewCell` and `UITableViewCell`.

You can also create your own conditions. It is just another closure that accepts the item to be styled and returns Bool whether to apply that style or not.

Here is an example of a `Condition` that matches depending on whether you installed the app on the Simulator, in a debug session on your device, via TestFlight or via the App Store. (Its not part of the framework so YMMV)

```swift
extension ViewStyle.Condition {
static func appDeploymentMethod (equals method: UIApplication.DeploymentMethod) -> ViewStyle.Condition {
return ViewStyle.Condition { _ in
return UIApplication.shared.deploymentMethod == method
}
}
}

extension UIApplication {
enum DeploymentMethod {
case debug, testFlight, simulator, appStore
}

var deploymentMethod: DeploymentMethod {
#if arch(i386) || arch(x86_64)
return .simulator
#elseif DEBUG
return .debug
#else
if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" {
return .testFlight
} else {
return .appStore
}
#endif
}
}
```

## Composing Styles

The real power of Salon comes in when you start combining and composing your conditional styles:

```swift
final class MyButton: UIButton {

override init(frame: CGRect) {
super.init(frame: frame)
self.applyStyles()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.applyStyles()
}

// Normally you'd put your styles in a separate file
func applyStyles () {
self.apply(styles:

// Our base style (`.style` is a convenience static var so you don't have to type `ViewStyle<UIButton>` repeatedly)
.style { button in
button.backgroundColor = .red
button.setTitleColor(.white, for: .normal)
button.setTitleColor(.red, for: .highlighted)
button.setTitleColor(.darkGray, for: .disabled)
},

// When disabled
.style(when: .controlState(equals: .disabled)) { button in
button.backgroundColor = .lightGray
},

// When highlighted
.style(when: .controlState(equals: .highlighted)) { button in
button.backgroundColor = .white
}
)
}

// Mark: Overrides

override var isEnabled: Bool {
didSet {
self.applyStyles()
}
}

override var isHighlighted: Bool {
didSet {
self.applyStyles()
}
}
}
```

Or when building universal apps:

```swift
extension ViewStyle {
static var myNavigationStack: UIStackView.Style {
return .compose(

// Common
.style { view in
view.axis = .horizontal
view.alignment = .firstBaseline
view.distribution = .fill
view.spacing = 36
},

// overrides for phone-based devices
.style(when: .horizontalSizeClass(equals: .compact)) { view in
view.axis = .vertical
view.spacing = 8
}
)
}
}
```


## Author

Rob Amos - [@bok_](https://twitter.com/bok_)

## License

Salon is available under the MIT license. See the LICENSE file for more info.
Loading

0 comments on commit 8409b1d

Please sign in to comment.