Useful code snippets, references and explanations to basic iOS topics. This repository is inspired in Paul Hudson's guide HackingWithSwift (100DaysOfSwift), all the challenges and Days can be found here, up to day 75 for now.
I will highly appreciate if you starred or followed this repo if it was of any help!
Some of HackigWithSwift challenges taken to a next level.
- README.md documentation
- Organize Layout Code
- CollectionView & Scroll
- Modern CollectionViews
- NavigationBar Apperance
- RoundedCorner & Shadow
- Corner masking
- UserDefaults
- Communication Patterns
- View Controller Heirarchy
- Font Size Guidelines
- ScrollView tricks
- Memory Leaks debugging
- View Controller LifeCycle
- Reducing memory footprint for UIImages
- Fix Massive View Controllers
- self vs. Self vs. Type
- MultipeerConnectivity (P2P)
- UICollectionViewCompositionalLayout
Get an object's ARC count
let objARC = Unmanages.passUnretained(:Instance).toOpaque()
Some IBOutlets may be loaded in a lazy manner, may not be loaded during awakeFromNib call, hence is prefered to be used only with UIView subclasses\
for case let CONDITIONS {}
// i.e
for case let node as SKSPriteNode in nodesAtPoint {}
MapKit annotation components
Name | Type |
---|---|
Title | String |
Subtitle | String |
Position | CLLocationCoordinate2D |
- Title
- Subtitle: String Set navigation back button, must be done on the NavigationVC pushing
self.navigationItem.backBarButtonItem = UIBarButtonItem(title:style:target:action)
Get image from URL with URLSession
func fetchImageFromURL(url: URL?) {
guard let url = url else { return }
// Its completion handler already runs in a background thread
URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
if let anError = error {
debugPrint("Data task error: \(anError.localizedDescription)")
}
guard let aData = data, let image = UIImage(data: aData) else {
return
}
if let httpResponse = response as? HTTPURLResponse {
debugPrint("Network response: \(httpResponse.statusCode)")
}
DispatchQueue.main.async { [weak self] in
self?.image = image
}
}).resume()
}
Get image from URL from Data object
func fetchImageFromURL(url: URL?) {
DispatchQueue.global(qos: .userInitiated).async {
if let data = try? Data(contentsOf: url) {
if let image = UIImage(data: data) {
DispatchQueue.main.async { [weak self] in
self?.image = image
}
}
}
}
}
Read JSON file from Bundle
guard let url = Bundle.main.url(forResource: "countries.json", withExtension: nil) else {
return
}
do {
let data = try Data(contentsOf: url)
let decodedData = try JSONDecoder().decode(Countries.self, from: data)
return decodedData
} catch {
print(error)
return Countries(countries: [Country]())
}
Prevent sloppy trainsitions when pushing/popping VCs by setting the background color.
view.backgroundColor = UIColor.white
Don't forget to deinit View Controllers when not used/seen on screen
private var picker: PHPicker? // initial nil reference to picker
// Calling picker
@objc private func importPicture() {
picker = Picker() // picker allocated
picker?.pickerDelegate = self
let pickerVC = picker?.photoPickerVC
guard let pickerVC = pickerVC else { return }
present(pickerVC, animated: true, completion: nil)
}
// Delegate method
func didSelectPicture(picture: UIImage) {
picker = nil // picker deallocated
}
// On root VC, deallocating the pushed VC before the main screen (UITable) appears
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated: animated)
// In case user didn't pick a picture, there is no PHPicker delegate for that
self.picker != nil { self.picker = nil }
}
Helps you create custom-complex UICollectionView layouts without needing to do manual calculations. It's composed of a Section, Groups and Items.
Allows devices to connect and share data (Message based, streaming, and resources) with each other automatically using 3 possible technologies:
- Wifi Connection (In case in the same Wifi)
- P2P Wifi
- Bluetooth
From iOS 13+ it's now necessary to add the following rows to info.plist. Where the 2 values in the Bounjour Services must be the registered serviceType with this format.
let serviceType: String = "lec-project25"
// In Bonjour services (in info.plist), without the quotes ofc ("")
let bonjourServices: [String] = [
"_lec-project25._tcp",
"_lec-project25._udp",
]
Type, references to the Metatype of a Type (The type of a type). This Type can change, as it's value, during compilation, might be different from execution. Also called "Dynamic Metatype". A reference to a Metatype allows you to use all of that type's properties and methods.
Value | Type |
---|---|
"Hello World" | String |
String.self | String.Type |
struct Person {
var name: String
}
let person1: Person = Person(name: "Leonardo")
// Where
// : Person represents the type of an instance
// Person(name: "Leonardo") represents an instance of the Person object
let something = type(of: person1) // Person.Type
let student: Person = something.init() // Yes, you can access the .init() from the MetaType reference "something".
// Dynamic Metatype
let developer: Any = "Leonardo"
// Any : During compilation
// String : During runtime
Is a reference to the current instance of a class or struct within itself. This can also be used to access the value of a Metatype. You probably have used before when working with UserDefaults to decode an object.
struct Book {
var title: String
var ISBN: String
}
// Reading new book from UserDefaults
let data = UserDefaults.standard.object(forKey: BOOK_KEY) as! Data
let savedBook = try! JSONDecoder().decode(book.self, from: data) // Accessing the value of Book's metatype.
Self's is mostly used for protocols & extensions, as it directly refers to the type which conforms the protocol. However, this may depend on wether constraints are used or not.
extension Numeric {
func squared() -> Self { // Self, refers to the type
// self Refers to "number"
return self * self
}
}
let number = 10
number.squared() // 100
// Constraint: Only elements that conform Numeric
// We can use Element as a "subtype" of the type of the element in the Sequence
extension Sequence where Element: Numeric {
// Remember Swift is a type-safe language, meaning we need to define the type of our value and referece types or let the runtime imply them. However, with methods we must explicity say what are we returning.
// With that in mind, returning Self alone would be like returning Sequence, when we know we must specify the Sequence's element's types.
func squareAll() -> [Self.Element] {
return self.map { $0 * $0 }
}
}
let numbers = [10, 11, 12]
numbers.squareAll() // [100, 121, 144]
Attributed Strings help applying multiple attributes to Strings, these attributes can also be reflected in i.e UILabel.text, and give larger settings compared to customizing a label as a UI object.
Asigns attributes to the whole text via a NSAttributedString dictionary.
let sentence: String = "Stone World"
let attributes: [NSAttributedString.Key: Any] = [
.foregroundColor: UIColor.white,
.backgroundColor: UIColor.systemRed,
.font: UIFont.boldSystemFont(ofSize: 22)
]
let attributedString = NSAttributedString(string: string, attributes: attributes)
UILabel.attributedText = attributedString
Allow having different attributes in different parts of the String.
let mutableAttributedString = NSMutableAttributedString(string: sentence)
mutableAttributedString.addAttribute(.backgroundColor, value: UIColor.systemGreen, range: NSRange(location: 0, length: sentence.count))
mutableAttributedString.addAttribute(.strikethroughStyle, value: 1, range: NSRange(location: 0, length: 5))
mutableAttributedString.addAttribute(.font, value: UIFont.systemFont(ofSize: 36), range: NSRange(location: 0, length: 5))
mutableAttributedString.addAttribute(.font, value: UIFont.systemFont(ofSize: 48), range: NSRange(location: 6, length: sentence.count - 6))
UILabel.attributedText = mutableAttributedString
Locations notifications don't depend on a dedicaded server capable of sending iOS notifications. These rely on local variables to trigger the notifications, the most common trigger is the interval or date component.
Grant access, no need of info.plist entry
// remember to import the notifications library
import UserNotifications
let center = UNUserNotificationCenter.current()
// thre are more options than these, and can be used simultaniously
center.requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
if let error = error {
print(error)
return
}
if granted {
// granted access to send notifications
} else {
// denied access to send notifications
}
}
Schedule local notifications
let center = UNUserNotificationCenter.current()
// Removes the schedule notifications that weren't triggered yet, depends on business logic
center.removeAllPendingNotificationRequests()
// notification's UI content and data
let content = UNMutableNotificationContent()
content.title = "Daily Reward"
content.body = "Come back and claim your daily reward"
content.categoryIdentifier = NotificationService.CATEGORY_IDENTIFIER
// custom optional data
content.userInfo = ["dailyReward": "+100 gems"]
content.sound = .default
// create the trigger condition
let trigger = UNTimeIntervalNotificationTrigger(timeInterval:repeats)
let request = UNNotificationRequest(identifier:content:trigger)
center.add(request:withCompletionHandler)
Delegate method to run when application was accessed from the notification
// here you can also access the optinal data used when the notification when scheduled
// must call its complentionHandler
// don't forget to set the UNUserNotificationCenter.current()'s delegate
func userNotificationCenter(center:response:completionHandler) -> Void
Customize category buttons for notification
let center = UNUserNotificationCenter.current()
// delegate protocol is UNUserNotificationCenterDelegate
center.delegate = self
let remindLater = UNNotificationAction(identifier: "remindLater", title: "Remind me later", options: .foreground)
// will connect to the notification already created by id: "alarm"
let category = UNNotificationCategory(identifier: "alarm", actions: [remindLater], intentIdentifiers: [], options: [])
center.setNotificationCategories([category])
Key | Value | Meaning |
---|---|---|
Supported interface orientations (iPhone) | Portrait (bottom home button) | Set portrait mode only |
Privacy - Camera Usage Description | Reason | Access device camera |
view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] // Top corners round
Should be called inside viewDidLayoutSubviews in a VC, but if not, an async call will do it in a UIView subclass.
- IMPORTANT, viewDidLayoutSubviews can be called multiple times without the subviews updating, hence you must prevent the shadowLayer from getting called endlessly by any layout update.
- Just call it once, otherwise multiple shadows will stack on top of each other and consume memory significally.
- You could make the CAShapeLayer optional and only run the code when it's nil this way it will be called only once per lifecycle. However, there are multiple ways to accomplish this same behavior, implement them as convenient.
DispatchQueue.main.async { [weak self] in
guard let strongSelf = self else { return }
strongSelf.shadowLayer = CAShapeLayer()
strongSelf.shadowLayer.path = UIBezierPath(roundedRect: strongSelf.cellContainer.bounds, cornerRadius: 10).cgPath
strongSelf.shadowLayer.fillColor = UIColor.white.cgColor
strongSelf.shadowLayer.shadowColor = UIColor.black.cgColor
strongSelf.shadowLayer.shadowPath = strongSelf.shadowLayer.path
strongSelf.shadowLayer.shadowOffset = .zero
strongSelf.shadowLayer.shadowOpacity = 0.2
strongSelf.shadowLayer.shadowRadius = strongSelf.shadowRadius
strongSelf.cellContainer.layer.insertSublayer(strongSelf.shadowLayer, at: 0)
}
- Simple solution, doesn't apply for subviews of class UIImageView
view.backgroundColor = UIColor.systemPink
view.layer.cornerRadius = 15
view.layer.shadowColor = UIColor.systemPink.cgColor
view.layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
view.layer.shadowRadius = 5
view.layer.shadowOpacity = 0.5
Comes with an embeded contentView inside the cell, it's size must not be edited, as it is done manually by the API.
Remove the Main interface from the Deployment Info section
Navigate inside the info.plist file.
Information Property List
- Application Scene Manifest
- Scene Configuration
- Application Session Role
- Item 0 (Default Configuration)
- Storyboard Name, delete its dictionary record (Minus button when clicked)
- Item 0 (Default Configuration)
- Application Session Role
- Scene Configuration
Inside the SceneDelegate.swift file write this code in the func scene(scene:willConnectTo:options) method. In a nutshell, you create a new window with the UIWindow object, then set its scene with the windowScene already unwrapped, set its main view controller and make it visible.
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
Used for saving minor data in the user's device. Data is located using a forKey as unique id in UserDefaults. It should not be used for large/complex/sensitive data, like images, it's not secure will slow the application launch with large data. Keychain is an option though. Models must conform Codable in order to code it's data into a JSON type.
✅ User Settings
✅ Program Settings
Writing to UserDefaults can be done in multiple ways for different value-types, but the most common is the object-type.
For the JSON approach, Person will have to conform Codable
class Person: Codable {
var name: String
var image: String
init(name: String, image: String) {
self.name = name
self.image = image
}
}
For the NSSKeyedArchive/Unarchived approach, Person will have to conform NSObject, NSCoding and NSSecureCoding. This approach allows objc compatibility.
class Person: NSObject, NSCoding, NSSecureCoding {
static var supportSecureCoding: Bool = true
var name: String
var image: String
init(name: String, image: String) {
self.name = name
self.image = image
}
// Required methods to conform the protocols
required init?(coder: NSCoder) {
self.name = coder.decodeObject(forKey: "name") as? String ?? ""
self.image = coder.decodeObject(forKey: "image") as? String ?? ""
}
func encode(with coder: NSCoder) {
coder.encode(self.name, forKey: "name")
coder.encode(self.image, forKey: "image")
}
}
And here is a new instance, an array of Person with only one element, example sake
let people = [Person(name: "Leonardo", image: "/Some/User/Image/Path")]
UserDefaults.standard.set(SomeObject, forKey: "custom-key")
Encode the data type into JSON format and then save it in UserDefaults under a key.
if let data = JSONEncoder().encode(people) {
UserDefaults.standard.set(data, forKey: "people")
}
Will transform any object value-type to a data value-type in order to safe to UserDefaults
let archive = try NSKeyedArchiver.archiveData(withRootObject: people, requiringSecureCoding: false)
UserDefaults.standard.set(archive, forKey: "people")
UserDefaults.standard.object(forKey: "people") as? Data
Read user defaults by key, then decode it into it's corresponding data-type structure.
if let loadedData = UserDefaults.standard.data(forKey: "people") {
if let decodedData = JSONDecoder().decode([Person].self, from loadedData) {
self.people = decodedData
}
}
Find the object in UserDefaults by key, cast it to Data and then unarchive it.
The ofClassed must contain NSString.self, CustomObject.self, and all other NS types in the model, or else it will result in console warnings of future deprecation.
let data = UserDefaults.standard.object(forKey: "people") as? Data
let unarchivedData = NSKeyedUnarchiver.unarchivedObject(ofClasses: [Person.self, NSArray.self, NSString.self], from: data) as? [Person]
self.people = unarchivedData
Optimize application support for Multi-core systems, by using different threads, and creating 4 different QoS (Quality of Service) queues. Which have different priorities for different multi-thread needs. The default queue higher than Utiliy but lower than User Initiated. HackingWithSwift reference.
Highest priority background thread. Should be used for work that is important to keep the user interface (UI) working.
i.e.: Automatic API request on a view, that a dashboard depends on.
Should be used to execute tasks requested by the user, which will be waiting for them to execute in order to continue using the app.
i.e.: Tapping on a button.
For long-running tasks that the user is aware of, but doesn't desperate needs it done quick and can continue doing other stuff on the app.
i.e.: Uploading files into Mega.
For long-running tasks that the user isn't aware of, nor cares when completed. Not important for the user.
i.e.: Sending statistical app information
DispatchQueue.global(qos: .userInitiated).async {
if let url = URL(string: urlString) {
if let data = try? Data(contentsOf: url) {
self.parse(json: data)
return
}
}
}
Try not to call inside a Dispatch queue
performSelector(inBackground: #selector(fetchData), with: url)
performSelector(onMainThread: #selector(fetchData), with: nil, waitUntilDone: false)