Categories
iOS Swift UIKit

Storing struct in UserDefaults

Structs can’t be directly stored in UserDefaults because UserDefaults does not know how to serialize it. As UserDefaults is backed by plist files, struct needs to be converted representation supported by it. The core idea boils down to the question, how to convert struct into Dictionary.

Converting struct to Dictionary

The most straight-forward way would be to manually create dictionary by adding all the properties one by one. Depending on the struct, it might get pretty long and also requires maintenance when changing the struct. Therefore, let’s take a look on a way of converting struct into JSON representation instead. This can be achieved by conforming to Encodable and using JSONEncoder and JSONSerialization. In the same way, we can convert Dictionary back to struct with JSONSerialization, JSONDecoder and conforming to Decodable. When conforming struct to Encodable and Decodable, Swift compiler will take care of generating default implementations for methods in those protocols. JSONEncoder and JSONDecoder use those methods for converting struct to Data and back. It should be noted that we could just store data in user defaults. But as data is not human-readable, let’s convert it to Dictionary instead.

struct Context: Codable {
let duration: TimeInterval
let view: String
}
struct SearchInfo: Codable {
let query: String
let numberOfMatches: Int
let context: Context
}
let searchInfos = [SearchInfo(query: "query1", numberOfMatches: 1, context: Context(duration: 1.0, view: "view1")),
SearchInfo(query: "query2", numberOfMatches: 2, context: Context(duration: 2.0, view: "view2"))]
// Converting to dictionary
extension SearchInfo {
var dictionaryRepresentation: [String: Any] {
let data = try! JSONEncoder().encode(self)
return try! JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
}
}
// Converting back to struct
extension SearchInfo {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return nil }
guard let info = try? JSONDecoder().decode(SearchInfo.self, from: data) else { return nil }
self = info
}
}
let defaults = UserDefaults()
// [Struct] -> [Dictionary]
let searchInfoDictionaries = searchInfos.map({ $0.dictionaryRepresentation })
// [Dictionary] to UserDefaults
defaults.set(searchInfoDictionaries, forKey: "Searches")
// [Dictionary] from UserDefaults
let dictionariesFromUserDefaults = defaults.array(forKey: "Searches")! as! [[String: Any]]
// [Dictionary] -> [Struct]
let convertedSearchInfos = dictionariesFromUserDefaults.compactMap({ SearchInfo(dictionary: $0) })

Adding DictionaryConvertible and DictionaryDecodable

This implementation can be made a bit more usable by using protocols and protocol extensions for providing default implementations. This makes it extremely easy to adopt this to any objects.

protocol DictionaryConvertible {
var dictionaryRepresentation: [String: Any] { get }
}
protocol DictionaryDecodable {
init?(dictionary: [String: Any])
}
typealias DictionaryRepresentable = DictionaryConvertible & DictionaryDecodable
extension DictionaryConvertible where Self: Encodable {
var dictionaryRepresentation: [String: Any] {
let data = try! JSONEncoder().encode(self)
return try! JSONSerialization.jsonObject(with: data, options: []) as! [String : Any]
}
}
extension DictionaryDecodable where Self: Decodable {
init?(dictionary: [String: Any]) {
guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return nil }
guard let info = try? JSONDecoder().decode(Self.self, from: data) else { return nil }
self = info
}
}
struct AutocompleteResult: Codable, DictionaryRepresentable {
let text: String
let suggestions: [String]
}

Summary

Using Swift’s Codable together with JSONEncoder, JSONDecoder and JSONSerialization we can skip writing code for converting data into different types and instead, providing a concise implementation for turning structs into dictionaries. We only talked about structs but this approach can be applied to classes as well.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Playground

StoringStructInUserDefaults (Xcode 10.2.1, Swift 5.0)

Categories
iOS Swift UIKit

Interactive animation with UIViewPropertyAnimator on iOS

UIViewPropertyAnimator enables configuring animations which can be modified when running. Animations can be paused and progress can be changed allowing to build interactive animations. UIViewPropertyAnimations are in stopped state by default. If we want to run the animation immediately, we can use class method runningPropertyAnimator(withDuration:delay:options:animations:completion:). UIViewPropertyAnimator gives us a lot of flexibility when it comes to composing animations and controlling them. Therefore let’s build an animation consisting of rotating and moving a view out of the visible rect.

Adding a view to animate

Firstly, we need to add a view which we are going to animate using UIViewPropertyAnimator. View is a UIView subclass which just overrides layerClass and returns CAGradientLayer instead. View is positioned into initial place using auto layout.

private lazy var animatingView: UIView = {
let view = GradientView(frame: .zero)
view.gradientLayer.colors = (1…3).map({ "Gradient\($0)" }).map({ UIColor(named: $0)!.cgColor })
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
view.addGestureRecognizer(gestureRecognizer)
view.addSubview(animatingView)
NSLayoutConstraint.activate([
animatingView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 72),
animatingView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -72),
animatingView.topAnchor.constraint(equalTo: view.topAnchor, constant: 72),
animatingView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -72)
])
}

Creating animations with UIViewPropertyAnimator

UIViewPropertyAnimator has several initialisers allowing to control the used timing function. In this example we’ll just use built-in ease in and ease out timing. This just means animation pace increases in the beginning and slows down at the end of the animation. In addition to mentioned UICubicTimingParameters (ease in and ease out), there is support for UISpringTimingParameters as well. Both timing parameters can be passed in using the convenience initialiser init(duration:timingParameters:). The animation is configured to rotate the view by 90 degrees and move the view following a spline created by current point of the view and two other points. When animation ends, we reset the transform and tell auto layout to update the layout which will just move the view back to the initial position.

private func makeAnimator() -> UIViewPropertyAnimator {
let animator = UIViewPropertyAnimator(duration: 2.0, curve: .easeInOut)
let bounds = view.bounds
animator.addAnimations { [weak animatingView] in
guard let animatingView = animatingView else { return }
animatingView.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 2.0)
UIView.animateKeyframes(withDuration: 2.0, delay: 0.0, options: .calculationModeCubic, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.4, animations: {
animatingView.center = CGPoint(x: bounds.width * 0.8, y: bounds.height * 0.85)
})
UIView.addKeyframe(withRelativeStartTime: 0.4, relativeDuration: 0.6, animations: {
animatingView.center = CGPoint(x: bounds.width + animatingView.bounds.height, y: bounds.height * 0.6)
})
})
}
animator.addCompletion({ [weak self] (_) in
guard let self = self else { return }
self.animatingView.transform = CGAffineTransform(rotationAngle: 0.0)
self.animator = nil
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
})
return animator
}
view raw Animator.swift hosted with ❤ by GitHub

Interrupting animation with UIPanGestureRecognizer

UIPanGestureRecognizer is used for interrupting the animation. When user starts dragging a finger on the screen, we capture the current animation progress and the initial point of the touch. Then, we can update the animation progress when dragging the finger to the left or right. When moving the finger back and forth, we can move the animation forward or backwards. As soon as letting the finger go, we start the animation which continues the animation from the update fractionComplete. The constant 300 is just a value defining the amount user needs to move the finger to be able to change the fractionComplete from 0.0 to 1.0.

private lazy var gestureRecognizer: UIPanGestureRecognizer = {
let recognizer = UIPanGestureRecognizer(target: self, action: #selector(updateProgress(_:)))
recognizer.maximumNumberOfTouches = 1
return recognizer
}()
@objc private func updateProgress(_ recognizer: UIPanGestureRecognizer) {
if animator == nil {
animator = makeAnimator()
}
guard let animator = animator else { return }
switch recognizer.state {
case .began:
animator.pauseAnimation()
fractionCompletedStart = animator.fractionComplete
dragStartPosition = recognizer.location(in: view)
case .changed:
animator.pauseAnimation()
let delta = recognizer.location(in: view).x – dragStartPosition.x
animator.fractionComplete = max(0.0, min(1.0, fractionCompletedStart + delta / 300.0))
case .ended:
animator.startAnimation()
default:
break
}
}

Summary

With UIViewPropertyAnimator we can build interactive animations with a very little code. Its API allows controlling the flow of the animations by pausing the animation and controlling the progress of the animation dynamically.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

InteractiveAnimation (Xcode 10.2, Swift 5)

Resources

Categories
iOS Swift UIKit

UICollectionViewFlowLayout and auto layout on iOS

UICollectionViewFlowLayout is layout object supplied by UIKit and enables showing items in grid. It allows customising spacings and supports fixed size cells and cells with different sizes. This time I am going to show how to build a collection view containing items with different sizes where sizes are defined by auto layout constraints.

Cell sizes in UICollectionViewFlowLayout

By default UICollectionViewFlowLayout uses itemSize property for defining the sizes of the cells. Another way is to use UICollectionViewDelegateFlowLayout and supplying item sizes by implementing collectionView(_:layout:sizeForItemAt:). Third way is to let auto layout for defining the size of the cell. This is what I am going to build this time.

Enabling auto layout based cell sizes

When setting UICollectionViewFlowLayout’s estimatedItemSize to UICollectionViewFlowLayout.automaticSize enables layout object to use auto layout.

final class CollectionViewController: UICollectionViewController {
let content: [String]
init(content: [String]) {
self.content = content
let layout = UICollectionViewFlowLayout()
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
super.init(collectionViewLayout: layout)
}
}

Setting up collection view cell with auto-layout constraints

When layout object is configured, second step is to create a cell. In the current prototype it is going to be a simple cell with a label and border around the label. Constraints are set up to have a 8 points space around the label. Constraints together with label’s intrinsicContentSize define the minimum size for the cell. If text is longer, intrinsicContentSize is wider.

final class TextCollectionViewCell: UICollectionViewCell {
let textLabel: UILabel
override init(frame: CGRect) {
textLabel = {
let label = UILabel(frame: .zero)
label.adjustsFontForContentSizeCategory = true
label.font = UIFont.preferredFont(forTextStyle: .body)
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
super.init(frame: frame)
contentView.addSubview(textLabel)
NSLayoutConstraint.activate([
textLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
textLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
textLabel.topAnchor.constraint(equalTo: topAnchor, constant: 8),
textLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8)
])
contentView.layer.borderColor = UIColor.darkGray.cgColor
contentView.layer.borderWidth = 1
contentView.layer.cornerRadius = 4
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

When putting all the things together, the end result is a collection view where every cell has its own size. Moreover, it supports dynamic type and cells will grow if user changes default text sizes.

Summary

UICollectionViewFlowLayout’s estimatedItemSize enables using auto-layout for defining cell sizes. Therefore creating cells where text defines the size of the cell is simple to do on iOS.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

FittingCellsInCollectionViewFlowLayout Xcode 10.2, Swift 5.0

Resources

Categories
Generics iOS Swift UIKit

Instantiating view controllers from UIStoryboard on iOS

There are several ways how to create user interfaces on iOS: programmatically, xib per view or using storyboard for multiple views. Personally I tend to use xibs and programmatically created layouts more but sometimes I also use UIStoryboard – depends on the situation on hand. Therefore I like to present two small additions to UIStoryboard API what makes instantiating view controllers more compact and convenient to use.

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "contacts") as! ContactsViewController

The approach I would like to present is a generic instantiate view controller method. It implies that in storyboard, the view controller’s identifier is set to the name of the view controller class.

When Storyboard ID for the view controller is set to the class name, it is now easy to derive the identifier from a type. It should be noted that in Swift, NSStringFromClass returns string with format <module name>.<class name>. Therefore it required to parse the returned string and taking only the class name part.

extension UIStoryboard {
func instantiateViewController<T: UIViewController>(withType type: T.Type) -> T {
let className = NSStringFromClass(type).split(separator: ".").last!
return instantiateViewController(withIdentifier: String(className)) as! T
}
}

In addition, with convenience method for getting storyboard, we can get to end result which is more compact and also avoids having identifier strings in the code.

extension UIStoryboard {
class var main: UIStoryboard {
return UIStoryboard(name: "Main", bundle: nil)
}
}
let viewController = UIStoryboard.main.instantiateViewController(withType: ContactsViewController.self)

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS Swift UIKit

UITableView swipe actions on iOS

Since iOS 11 it is easy to add swipe actions in a table view. What we need to do, is to implement UITableView delegate methods for providing actions for leading and trailing edge. Let’s jump right into it.

Default swipe actions

Swipe actions are provided by two delegate methods which return UISwipeActionsConfiguration. It should be mentioned that when returning nil in those delegates, table view will use its default set of actions. Default action is delete action and acton should be implemented in the editingStyle delegate method.

optional func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
optional func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
showAlert("Delete triggered")
case .insert, .none:
break
}
}

Custom swipe actions

Let’s leave default actions aside and see what is this configuration object swipe action delegates need to return. It just provides the set of different actions and a property of controlling if the full swipe triggers the first action or not. By default, this is turned on and full swipe triggers the first action. Action itself is represented by UIContextualAction class. This class defines handler block, title, image and background color of the action. Action can’t have both image and title, whenever image is set, title is ignored. In addition, action’s initialiser defines style: normal style means light grey background and destructive style uses red background. One needs to be aware that when adding too many actions results in having overlapping action buttons in the table view. Therefore it always makes to test and see if the amount of actions really works on the smallest display size.

override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let item = content[indexPath.item]
switch item {
case .iconAction:
let action = UIContextualAction(style: .normal, title: item.rawValue, handler: { [weak self] (action, view, block) in
self?.showAlert(item.rawValue)
})
action.backgroundColor = UIColor(hue: 0.11, saturation: 0.56, brightness: 0.48, alpha: 1.0)
action.image = UIImage(named: "Icon")
return UISwipeActionsConfiguration(actions: [action])
case .multipleTrailingActions:
let action1 = UIContextualAction(style: .normal, title: "Action1", handler: { [weak self] (action, view, block) in
self?.showAlert("Action1")
})
action1.backgroundColor = UIColor(hue: 0.56, saturation: 0.56, brightness: 0.55, alpha: 1.0)
let action2 = UIContextualAction(style: .normal, title: "Action2", handler: { [weak self] (action, view, block) in
self?.showAlert("Action2")
})
action2.backgroundColor = UIColor(hue: 0.35, saturation: 0.33, brightness: 0.55, alpha: 1.0)
return UISwipeActionsConfiguration(actions: [action1, action2])
case .trailingAction:
let action = UIContextualAction(style: .normal, title: item.rawValue, handler: { [weak self] (action, view, block) in
self?.showAlert(item.rawValue)
})
return UISwipeActionsConfiguration(actions: [action])
case .trailingDestructiveAction:
let action = UIContextualAction(style: .destructive, title: item.rawValue, handler: { [weak self] (action, view, block) in
self?.showAlert(item.rawValue)
})
return UISwipeActionsConfiguration(actions: [action])
case .trailingTableViewDefaultAction:
return nil
case .leadingAction, .leadingDestructiveAction, .tooManyLeadingActions:
return UISwipeActionsConfiguration(actions: []) // when returning nil, default actions are shown
}
}

Summary

In summary, swipe actions are extremely simple to add using table view delegates. It is an easy way of providing important actions in an accessible manner.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

UITableViewSwipeActions Xcode 10.2, Swift 4.2

Resources

Categories
iOS Swift

Circle shaped collection view layout on iOS

UICollectionViewLayout’s responsibility is to define layout: all the cell locations and sizes. It acts like a data source object which provides layout related information to the collection view. Collection view then uses that information for creating cells and placing them on screen. This time we’ll take a look on how to create a custom circle shaped layout.

Creating UICollectionViewLayout subclass

First step for creating a custom layout is to create UICollectionViewLayout subclass. Then we need to define the size of the layout. In this simple case, all the cells will fit into the available area – therefore we can just return the collection view’s own size.

override var collectionViewContentSize: CGSize {
return collectionView?.bounds.size ?? .zero
}

When-ever user rotates the device, the size of the collection view changes, therefore we would like to reset the layout.

override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}

Prepare is called whenever layout gets invalidated and it is a best place where to pre-calculate cell locations. Then it is possible to return attributes in all of the other methods querying for attributes later on. This gives us the best possible performance.

Items are placed along a circle which is centred in the collection view. As collection view can be under navigation bar, then the frame of the circle needs to take account contentOffset. Moreover, as centre points of items are on the circle, we also need to take account the item size. Calculating positions for items is just a matter of applying simple trigonometry with right-angled triangles.

private var layoutCircleFrame = CGRect.zero
private let layoutInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
private let itemSize = CGSize(width: 100, height: 100)
private var itemLayoutAttributes = [UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
itemLayoutAttributes.removeAll()
layoutCircleFrame = CGRect(origin: .zero, size: collectionViewContentSize)
.inset(by: layoutInsets)
.insetBy(dx: itemSize.width / 2.0, dy: itemSize.height / 2.0)
.offsetBy(dx: collectionView.contentOffset.x, dy: collectionView.contentOffset.y)
.insetBy(dx: -collectionView.contentOffset.x, dy: -collectionView.contentOffset.y)
for section in 0..<collectionView.numberOfSections {
switch section {
case 0:
let itemCount = collectionView.numberOfItems(inSection: section)
itemLayoutAttributes = (0..<itemCount).map({ (index) -> UICollectionViewLayoutAttributes in
let angleStep: CGFloat = 2.0 * CGFloat.pi / CGFloat(itemCount)
// CGRect extension for center and innerRadius: https://gist.github.com/laevandus/07955ff394984bda6de4922734429c84
var position = layoutCircleFrame.center
position.x += layoutCircleFrame.size.innerRadius * cos(angleStep * CGFloat(index))
position.y += layoutCircleFrame.size.innerRadius * sin(angleStep * CGFloat(index))
let indexPath = IndexPath(item: index, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = CGRect(center: position, size: itemSize)
return attributes
})
default:
fatalError("Unhandled section \(section).")
}
}
}

If we have pre-calculated attributes, then it is straight-forward to return them in the layout attributes methods. When adding or removing items, cells will move to new location with a nice animation by default.

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return itemLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let numberOfItems = collectionView?.numberOfItems(inSection: indexPath.section) ?? 0
guard numberOfItems > 0 else { return nil }
switch indexPath.section {
case 0:
return itemLayoutAttributes[indexPath.item]
default:
fatalError("Unknown section \(indexPath.section).")
}
}

I would like to highlight the fact that content presented here is just a bare minimum for setting up custom collection view layout. UICollectionView is highly configurable.

Setting up collection view

When custom collection view layout is ready, it is time to hook it up to the collection view. Custom collection view layout can be set in storyboard or when initialising collection view or collection view controller.

Setting custom layout in UIStoryBoard

Implementing a basic collection view controller for just showing one type of cells requires only a little bit of code. Data source methods provide the count of the items and sections and return configured collection view cells.

final class CollectionViewController: UICollectionViewController {
// MARK: Managing the View
enum ReuseIdentifier: String {
case item = "ItemCell"
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView!.register(CircleCollectionViewCell.self, forCellWithReuseIdentifier: ReuseIdentifier.item.rawValue)
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addItem))
}
// MARK: Managing the Content
private var items: [UIColor] = [.random(), .random(), .random()]
@objc private func addItem() {
items.append(.random())
collectionView.insertItems(at: [IndexPath(item: items.count – 1, section: 0)])
}
// MARK: Collection View Data Source
override func numberOfSections(in collectionView: UICollectionView) -> Int {
return items.count > 0 ? 1 : 0
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ReuseIdentifier.item.rawValue, for: indexPath) as! CircleCollectionViewCell
cell.backgroundView?.backgroundColor = items[indexPath.item]
cell.titleLabel.text = "\(indexPath.item)"
return cell
}
}

Summary

Adding custom collection view layout requires subclassing UICollectionViewLayout and overriding several methods. The core idea is to provide UICollectionViewAttributes for every cell which UICollectionView will then use for creating and laying out cells.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

CustomCollectionViewLayout (GitHub), Xcode 10.1, Swift 4.2

Resources

Categories
CoreAnimation iOS Swift

Particle emitters for apps with CAEmitterLayer on iOS

Particle emitters are encountered the most often in games. Apple’s SpriteKit framework contains SKEmitterNode for this exact purpose. Moreover, Xcode even has a live editor for configuring SKEmitterNode. As SKEmitterNode can only be added to SKScene, it is not a good fit for apps. But no worries, apps can take advantage of CAEmitterLayer which is part of the CoreAnimation framework. CAEmitterLayer is subclass of CALayer and therefore we can add it to anywhere in our UIKit based apps. Snowboarding and skiing related apps could add a cool snow effect with a little bit of code.

CAEmitterLayer and CAEmitterCell

Particles used by CAEmitterLayer are defined by CAEmitterCell. Every emitter layer can have multiple cells and every cell can have it’s own subcells as well. Meaning every particle has an ability of emitting other particles. CAEmitterCell itself is just a definition of the particle: how it moves, looks and lives.

extension CAEmitterLayer {
convenience init(image: CGImage) {
self.init()
let cell: CAEmitterCell = {
let cell = CAEmitterCell()
cell.birthRate = 40
cell.contents = image
cell.emissionLongitude = .pi / 2.0
cell.emissionRange = CGFloat.pi / 4.0
cell.lifetime = 16
cell.scale = 0.3
cell.scaleRange = 0.2
cell.velocity = 80
return cell
}()
emitterCells = [cell]
}
}

This example presents a way of how to create a simple CAEmitterLayer what only has one cell with predefined image. Here we can see that cell defines birthrate: (particles per second), scale and its variance, lifetime in seconds, velocity, and emission direction. Emission longitude and range define a cone in which particles are emitted: if range is zero, all the particles have the same direction (emissionLongitude) and if range is 2 * pi, then particles are emitted in all the directions.

CAEmitterLayer also several different emitter shapes: circle, cuboid, line, point, rectangle, sphere (checkout the example project down below). It should be noted those shapes are defined by emitterPosition, emitterDepth, emitterSize, emitterZPosition and therefore, if you want to move emission location, update emitterPosition, not layer’s position.

Adding emitter layer

On iOS every UIView is backed by an instance of CALayer. As CAEmitterLayer is also a layer then adding it to view hierarchy is as simple as adding a sublayer to view’s backing layer.

override func viewDidLoad() {
super.viewDidLoad()
emitterView.layer.addSublayer({
guard let image = UIImage(named: "Spark")?.cgImage else { fatalError("Failed loading image.") }
let emitterLayer = CAEmitterLayer(image: image)
emitterLayer.emitterPosition = emitterView.bounds.center
emitterLayer.name = "Emitter"
return emitterLayer
}())
}
extension CGRect {
var center: CGPoint {
return CGPoint(x: midX, y: midY)
}
}

Summary

CAEmitterLayer is an efficient way of adding particle systems to any app. Setting up emitter layer required to define particle(s) and then adding the CAEmitterLayer to the view hierarchy. With a little bit of code, we can add playful touch to any app. Only if I could design an awesome looking particle emitter. 🤔

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

CAEmitterLayer (GitHub), Xcode 10.1, Swift 4.2

Resources

Categories
iOS Swift

Most visited posts in 2018

It is this time of the year when to look back. In total I published 22 blog posts and had 2765 views by 1632 people. Together with 3 posts from the 2017 I now have 25 blog posts.

In the end of the July I posted Sharing UI code with protocol extension what marked the beginning of the biweekly schedule. Since then I have tried to find a topic for every second week and write a concise post with sample code. The aim is to share the knowledge in a compact format and have some ready to use sample code what can be used in your projects. I would like to remind that all the source code is under MIT license so feel free to use as you would like to.

Top 3

My hope is that at least some people found those posts useful and they learned something new. Feel free to contact me on Twitter @toomasvahter. Thank you for reading.

Categories
iOS Swift UIKit

Displaying images efficiently on iOS

Loading an image and displaying it on a screen consists of several steps. Firstly, we need to load the image data into memory, then decoding it to pixel data and finally, telling GPU to display it on screen. The whole process can be as short as two lines of code: creating an instance of UIImage using the name of the image and then assigning it to an UIImageView. Simple, but not so efficient.

Memory consumption impacts

Memory management is important topic as misusing memory can lead to, in worse case, system terminating our app. In addition, using too much memory will  cause high system CPU usage due to it trying to make more memory available by compressing it. Moreover, high CPU will lead to shorter battery life and no-one is happy about it.

High memory usage can be caused by keeping whole images in the memory and letting GPU to downscale it. The more efficient approach is to create a thumbnail with the size of the image view. This approach will use the minimum amount of pixel data and therefore system will use less resources.

Creating a thumbnail

For keeping resource consumption low, lets create UIImage extension for loading and creating the image at URL with specified size.

extension UIImage {
convenience init?(thumbnailOfURL url: URL, size: CGSize, scale: CGFloat) {
let options = [kCGImageSourceShouldCache: false] as CFDictionary
guard let source = CGImageSourceCreateWithURL(url as CFURL, options) else { return nil }
let targetDimension = max(size.width, size.height) * scale
let thumbnailOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceThumbnailMaxPixelSize: targetDimension] as CFDictionary
guard let thumbnail = CGImageSourceCreateThumbnailAtIndex(source, 0, thumbnailOptions) else { return nil }
self.init(cgImage: thumbnail)
}
}

Thumbnail creation consists of a couple of steps. Firstly, we create an instance of CGImageSource and tell it not to load and decode the data immediately (by setting kCGImageSourceShouldCacheImmediately to false). Instead, we pass the source into the thumbnail creation method which will immediately process the image data and scale it to the appropriate size. This approach avoids keeping the whole image in memory and instead, just uses the unscaled version.

private func loadThumbnailImage() {
let size = imageView.bounds.size
let scale = traitCollection.displayScale
let url = Bundle.main.url(forResource: "Wallpaper", withExtension: "jpg")!
DispatchQueue.global(qos: .userInitiated).async {
let image = UIImage(thumbnailOfURL: url, size: size, scale: scale)!
DispatchQueue.main.async { [weak self] in
self?.imageView.image = image
}
}
}

In a bit extreme example: displaying a thumbnail of a 5120 by 2880 pixels JPG image makes the app’s memory usage to be around 7 MB compared to 28 MB when the whole image is in memory. But on the other hand, we can have an app with multiple image views, each of them displaying a much larger image. Depending on the app, the difference can be huge.

Summary

We took a look at issues what can be caused by excessive use of system resources when displaying images. Then, we added an extension to UIImage for loading a larger image and scaling it to the size it is going to displayed. Small change, but has a huge impact.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

UIImageThumbnail (GitHub) Xcode 10.1, Swift 4.2

Resources

Image and Graphics Best Practices (Apple)

Categories
iOS Swift UIKit

Observing keyboard visibility on iOS

Almost every app needs a way of inserting information using keyboard. When keyboard shows up, we do not want to keep content behind the keyboard hidden and instead, allow user to see it. UIResponder contains several notifications we can use to adjust the layout.

Keyboard change notifications

UIResponder contains a list of notifications and user info keys. We have notifications for reacting to visibility and frame changes (for example when rotating device). Notification’s userInfo contains a variety of information about the change. What makes this API a little bit difficult to use is parsing the user info every time we need to use those notifications. If we need to observe keyboard in several view controllers then the amount of code of setting up observation and doing type casting starts to build up. Therefore it makes more sense to have an object handling the observation and type casting user info keys.

extension UIResponder {
public class let keyboardWillShowNotification: NSNotification.Name
public class let keyboardDidShowNotification: NSNotification.Name
public class let keyboardWillHideNotification: NSNotification.Name
public class let keyboardDidHideNotification: NSNotification.Name
public class let keyboardWillChangeFrameNotification: NSNotification.Name
public class let keyboardDidChangeFrameNotification: NSNotification.Name
public class let keyboardFrameBeginUserInfoKey: String // NSValue of CGRect
public class let keyboardFrameEndUserInfoKey: String // NSValue of CGRect
public class let keyboardAnimationDurationUserInfoKey: String // NSNumber of double
public class let keyboardAnimationCurveUserInfoKey: String // NSNumber of NSUInteger (UIViewAnimationCurve)
public class let keyboardIsLocalUserInfoKey: String // NSNumber of BOOL
}

Using a KeyboardObserver

KeyboardObserver is a lightweight object observing keyboard related notifications and calling the changeHandler when any of the notifications is received. User info and notification type information is represented with struct Info. Before looking into how it is implemented, let’s take a look on the example.

final class ViewController: UIViewController {
@IBOutlet weak var scrollView: UIScrollView!
private var keyboardObserver: KeyboardObserver?
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
keyboardObserver = KeyboardObserver(changeHandler: { [weak self] (info) in
guard let self = self else { return }
switch info.event {
case .willShow:
self.scrollView.contentInset.bottom = info.endFrame.height
case .willHide:
self.scrollView.contentInset.bottom = 0
default:
break
}
})
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
keyboardObserver = nil
}
}

Here we can see that setting up observer is straight-forward and accessing end frame of the keyboard does not require any type casting. Compare it with adding observers to those notifications and then using conditional casts for getting relevant information in the view controller. 

Creating a KeyboardObserver

But let’s now take a look on how it is implemented and see how much less code we need to write in the future. KeyboardObserver is initialised with a changeHandler closure like seen in the previous paragraph. Initialiser retains the handler and sets up observers for all the relevant notifications. For simplicity, we are observing all the notifications but it would also be possible to have an extra argument defining a set of Events and then observing only the notifications we really want to react to.

Type casting relies completely on promises made by UIKit. As UIKit promises that user info always contains those values, we can avoid having any optional values in the Info struct. Therefore it is simpler to use the struct later on as no unwrapping is required.

final class KeyboardObserver {
enum Event {
case willShow, didShow, willHide, didHide, willChangeFrame, didChangeFrame
}
struct Info {
let animationCurve: UIView.AnimationCurve
let animationDuration: TimeInterval
let isLocal: Bool
let beginFrame: CGRect
let endFrame: CGRect
let event: Event
}
let changeHandler: (Info) -> ()
init(changeHandler: @escaping (Info) -> ()) {
self.changeHandler = changeHandler
let notifications: [Notification.Name] = [UIResponder.keyboardWillShowNotification,
UIResponder.keyboardDidShowNotification,
UIResponder.keyboardWillHideNotification,
UIResponder.keyboardDidHideNotification,
UIResponder.keyboardWillChangeFrameNotification,
UIResponder.keyboardDidChangeFrameNotification]
notifications.forEach { (notification) in
NotificationCenter.default.addObserver(self, selector: #selector(KeyboardObserver.keyboardChanged(_:)), name: notification, object: nil)
}
}
@objc private func keyboardChanged(_ notification: Notification) {
guard let userInfo = notification.userInfo else { fatalError() }
let event: Event = {
switch notification.name {
case UIResponder.keyboardWillShowNotification: return .willShow
case UIResponder.keyboardDidShowNotification: return .didShow
case UIResponder.keyboardWillHideNotification: return .willHide
case UIResponder.keyboardDidHideNotification: return .didHide
case UIResponder.keyboardWillChangeFrameNotification: return .willChangeFrame
case UIResponder.keyboardDidChangeFrameNotification: return .didChangeFrame
default:
fatalError("Unknown change notification \(notification).")
}
}()
changeHandler(Info(event: event, userInfo: userInfo))
}
}
fileprivate extension KeyboardObserver.Info {
init(event: KeyboardObserver.Event, userInfo: [AnyHashable: Any]) {
self.event = event
animationCurve = {
let rawValue = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as! Int
return UIView.AnimationCurve(rawValue: rawValue)!
}()
animationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as! TimeInterval
isLocal = userInfo[UIResponder.keyboardIsLocalUserInfoKey] as! Bool
beginFrame = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as! CGRect
endFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! CGRect
}
}

Summary

We took a look on how to avoid observing multiple notifications and type casting notification user info values on the view controller level. Instead, we created a separate object handling observing and type casting and gives us a simple and concise API to work with.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

KeyboardObserver Xcode 10.1, Swift 4.2

References

UIResponder (Apple)