-
SwiftUI Tap Anywhere
Today I learned how to implement tap anywhere to dismiss the keyboard on SwiftUI.
In your parent stack, either
VStack
orHStack
, add the following code.1 2 3 4
.contentShape(Rectangle()) .onTapGesture { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) }
The most important thing to take note here is the contentShape modifier or tap gesture will not work.
-
Xylophone App
In roughly an hour or two, I created an iOS app for our son who loves playing his Xylophone. I came upon this old repo for inspiration and reference. This was still developed using Storyboard and UIKit. So, I set out to write my own using SwiftUI.
I also borrowed their sound files. The owner of these files belongs to them.
The code looks like this initially.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
// // PlayerView.swift // Xylo // // Created by Lawrence Gimenez on 4/23/23. // import SwiftUI import AVFoundation var audioPlayer: AVAudioPlayer! struct PlayerView: View { var body: some View { VStack { Button(action: { play(sound: "C") }) { Text("C") .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(Color.red) Button(action: { play(sound: "D") }) { Text("D") .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(Color.orange) Button(action: { play(sound: "E") }) { Text("E") .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(Color.yellow) Button(action: { play(sound: "F") }) { Text("F") .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(Color.green) Button(action: { play(sound: "G") }) { Text("G") .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(Color.teal) Button(action: { play(sound: "A") }) { Text("A") .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(Color.indigo) Button(action: { play(sound: "B") }) { Text("B") .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(Color.purple) Button(action: { play(sound: "C") }) { Text("C") .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(Color.red) } .frame(maxWidth: .infinity, maxHeight: .infinity) } private func play(sound: String) { let _ = print("Play \(sound)") let url = Bundle.main.url(forResource: sound, withExtension: ".wav") audioPlayer = try! AVAudioPlayer(contentsOf: url!) audioPlayer.prepareToPlay() audioPlayer.play() } } struct PlayerView_Previews: PreviewProvider { static var previews: some View { PlayerView() } }
And here’s what it looks like on the device.
If you notice, the code is so long and redundant. We can do better. Let’s refactor our code by creating a model class so we can use it inside a loop.
Refactor Time
Let’s create a Key model. I imported SwiftUI since I need the Color class.
1 2 3 4 5 6 7 8
import SwiftUI struct Key: Identifiable { var id: Int var note: String var color: Color }
Go back to our
PlayerView
class and let’s create an array based on the Key model.1 2 3 4 5 6 7 8 9 10
private var arrayKeys = [ Key(id: 0, note: "C", color: Color.red), Key(id: 1, note: "D", color: Color.orange), Key(id: 2, note: "E", color: Color.yellow), Key(id: 3, note: "F", color: Color.green), Key(id: 4, note: "G", color: Color.teal), Key(id: 5, note: "A", color: Color.indigo), Key(id: 6, note: "B", color: Color.purple), Key(id: 7, note: "C", color: Color.red) ]
Let’s update our body based on our newly created array.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
var body: some View { VStack { ForEach(arrayKeys) { key in Button(action: { play(sound: key.note) }) { Text(key.note) .foregroundColor(.white) .frame(maxWidth: .infinity, minHeight: 90) } .background(key.color) } } .frame(maxWidth: .infinity, maxHeight: .infinity) }
Much better. Code is now shorter, if I want to add another key, I would just add another Key object to our array.
You can check the whole source code here. The project is compiled using the latest Xcode 14.3.
-
SwiftUI: How to use NavigationStack inside the .toolbar
First, why do I mostly find a solution after posting a Stackoverflow question?
So most examples and tutorials only use
NavigationStack
andNavigationLink
inside aList
. I’m surprised by how nobody seems to implement using the.toolbar
modifier.The solution was a little straightforward I found out.
Instead of
NavigationView
, you use the new and shinyNavigationStack
. And use.navigationDestination()
instead ofNavigationLink
.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
@State private var goToSettings = false NavigationStack { ZStack { // Some more codes } .toolbar { Button(role: .destructive, action: { goToSettings = true }) { Label("Settings", systemImage: "gearshape.fill").foregroundColor(colorForeground) } } .navigationDestination(isPresented: $goToSettings, destination: { SettingsView() }) }
I need to get more familiar with SwiftUI’s modifiers as it is still a little confusing for me.
-
SwiftUI Journey Part 11: Passing parameters
Right now I am still figuring out how to pass a parameter from one
View
to another. I read I need to use@Binding
but don’t know what it is. Or what differs it from@State
. Currently, I am still studying@Binding
.All the tutorials and Apple documentation look good but they did not include what to do with the compile error in
PreviewProvider
.Well, it seems
PreviewProvider
is separate and you can pass static variables different from theView
class.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
import SwiftUI struct DetailsView: View { @Binding var url: String var body: some View { Text("Hello, World!") } } struct DetailsView_Previews: PreviewProvider { @State static var urlPreview: String = Urls.main.advanceSearch static var previews: some View { DetailsView(url: $urlPreview) } }
Compiled successfully.
-
SwiftUI Journey Part 10: Settings
I can’t believe how easy it is to implement a settings page in SwiftUI. For the UI, I only need 37 lines.
Of course, this implementation won’t be accepted by the programming gods. Why you may ask. This is implemented in a static way. What if you want to turn every
Text()
background toColor.yellow
. Then you will have to add the modifier background to everyText()
view.Let’s make it dynamic.
Ah much better.
-
UIKit Drawing Tutorial Fix
I tried the tutorial from raywenderlich.com but the behavior was incorrect. The canvas or
UIImageView
seems to move after lifting my finger.It tried to run the official project from raywenderlich.com but it has the same strange behavior.
Luckily I found a solution, you need to change this line inside
touchesEnded
function// from UIGraphicsBeginImageContext(mainImageView.frame.size) // change to UIGraphicsBeginImageContext(view.frame.size)
And that's it.
-
iOS 15 Button Title
If you encountered default titles on your UIButton when using setImage() like my situation below.
As you can see on the screenshot below, I removed the title. So everything should be alright.
And when I run it on my iOS 15 device.
What the. Anyway, solution is simple. On your UIButton's Atrribute inspector, set the style to default and remove the default Button title.
Let me know if you have any questions.
-
OnlineJobs for iOS (v4.5.2) Bug
Last week, it was one of those times where you know something went wrong when suddenly you get multiple Slack notifications. I received a Slack notification for every minute, I had to mute the channel.
When I open the Bugsnag dashboard, I could see it was affecting hundreds of users. As of this moment, 154 users. Yikes!
What took me so long was that Bugsnag wasn't able to capture all the other threads. I had to go back to Xcode's Organizer Reports. I've been recently relying more on Xcode's crash reports because they tend to be more detailed and well integrated with Xcode.
The bug was somewhere in this code block. This is for parsing an HTML String and it was crashing right on viewDidLoad() function.
extension Data { var html2AttributedString: NSAttributedString? { do { return try NSAttributedString(data: self, options: [ .documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue ], documentAttributes: nil) } catch { return nil } } var html2String: String { return html2AttributedString?.string ?? "" } } extension String { var html2AttributedString: NSAttributedString? { return Data(utf8).html2AttributedString } var html2String: String { return html2AttributedString?.string ?? "" } }
Then I realized since in this ViewController I did not present this as full screen but in a modal kind of way, somehow it was affecting the lifecycle. The fix I did was to transfer the HTML parsing to viewDidAppear() function to give time to parse the HTML. ViewDidAppear is the callback that tells the ViewController that the views are added to the view hierarchy.
override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) lastMessageLabel.text = message?.body?.html2String }
That was it, the simple fix. Sorry guys, please download the latest OnlineJobs update v4.5.3.
If you like to receive daily mobile development posts, you can subscribe below. I’ll be posting frequently.
-
Dynamic TableViewCell Height
I'm going to demonstrate on how I achieved the dynamic height for my
UITableViewCell
. Below is what it looks like.This is a
UITableViewCell
for comments. ImageView on the left side is for the user's avatar, the top mostUILabel
is for the username and lastly at the bottom is theUILabel
for comment.Yes don't mind the red warnings, that is by design. The important here is for the
UITableViewCell
height to expand with AutoLayout.First take note of the UIImageView's constraints.
Next, the constraints for the username UILabel. As you may notice I did not specified a Height constraint, thus the red error for both UILabels.
And lastly the constraints for our comment UILabel.
Now in your code make sure you set the
rowHeight
andestimatedRowHeight
toUITableView.automaticDimension
commentsTableView.rowHeight = UITableView.automaticDimension commentsTableView.estimatedRowHeight = UITableView.automaticDimension
-
Code Refactoring: First Loom Video
In programming, you always encounter duplicate code blocks. The best thing to do is to place it in a separate function so that we can reuse it throughout our source code.
-
Play Looping Video using Swift
Playing a looping video in iOS is fairly simple but there's a catch. You need to declare
AVPlayerLooper
global in your class.Below is the code:
let avPlayerItem = AVPlayerItem(url: videoOutputURL!) let avQueuePlayer = AVQueuePlayer(playerItem: avPlayerItem) let avPlayerLayer = AVPlayerLayer(player: avQueuePlayer) avPlayerLooper = AVPlayerLooper(player: avQueuePlayer, templateItem: avPlayerItem) avPlayerLayer.frame = videoPlayContainerView.bounds avPlayerLayer.videoGravity = .resizeAspectFill avQueuePlayer.actionAtItemEnd = .none videoPlayContainerView.layer.addSublayer(avPlayerLayer) avQueuePlayer.play()
The
videoPlayContainerView
is aUIView
. -
Create a camera shutter in Swift
I'm going to show how I achieved the camera shutter button and animation using Swift, more or less the same with the stock iOS camera app. Grab a coffee because this one's going to be a little longer.
Storyboard UI
On your ViewController, drag a UIButton and place it at the bottom part. On the Size Inspector, give it a width and height of 65, thus creating a perfect square. Add the width and height constraint, also the bottom anchor.
And, also the horizontal alignment constraint to 0.
Before we proceed, there is a flaw in our UI implementation. As you noticed we shouldn't use the bottom constraint because once we implement the scale or translate animation it will hold off and pull down the UIButton back to its proper place regardless of its size. What we would like to do is for UIButton to maintain its screen position regardless of its size.
So let's add a container UIView that will house our UIButton. Drag another UIView and this time make sure it fills up the majority of the bottom area. In the Size Inspector the values should be: x=4, y=741, width=406, height=100. After, add the necessary constraints as you see fit. Place the UIButton inside this newly added container UIView. It should look like the one below.
Update the UIButton constraints with both vertically and horizontally constraints. And now we don't have to worry our UIButton not scaling properly when resizing.
Now, for the border that will surround our UIButton. Drag a UIView inside the container UIView and behind the UIButton and make sure it is much more larger in terms of width and height. Set both width and height to 80. For our border UIView, add the following width and height constraints and also the horizontally and vertically constraint, same with our UIButton. It should look like the one below.
Coding Time
Now let's code. Let's wire the outlets to our ViewController. Also, include the width and height constraint of our UIButton.
class CustomCameraViewController: UIViewController { @IBOutlet weak var captureButton: UIButton! @IBOutlet weak var captureButtonWidthConstraint: NSLayoutConstraint! @IBOutlet weak var captureButtonHeightConstraint: NSLayoutConstraint! @IBOutlet weak var captureBorderView: UIView! private let videoCaptureOutput = AVCaptureMovieFileOutput() }
In our viewDidLoad, let's turn our squares (both capture UIButton and border UIView) into circles to signify a ready to record status.
override func viewDidLoad() { super.viewDidLoad() captureButton.layer.cornerRadius = captureButton.frame.size.width / 2 captureButton.clipsToBounds = true captureBorderView.layer.borderWidth = 6.0 captureBorderView.layer.borderColor = UIColor.red.cgColor captureBorderView.layer.cornerRadius = captureBorderView.frame.size.width / 2 }
Next, wire the IBAction from our capture UIButton. And below is all the code and as you can see I added a pulse animation just to make it more clearer the recording status.
@IBAction func captureTapped(_ sender: Any) { toggleVideoRecording() } private func toggleVideoRecording1() { if !videoCaptureOutput.isRecording { DispatchQueue.main.async { UIView.animate(withDuration: 0.2, animations: { self.captureButtonWidthConstraint.constant = 35 self.captureButtonHeightConstraint.constant = 35 self.captureButton.layer.cornerRadius = 9 UIView.animate(withDuration: 0.4, animations: { let pulseAnimation = CABasicAnimation(keyPath: "opacity") pulseAnimation.duration = 0.5 pulseAnimation.fromValue = 0 pulseAnimation.toValue = 1 pulseAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) pulseAnimation.autoreverses = true pulseAnimation.repeatCount = .infinity self.captureBorderView.layer.add(pulseAnimation, forKey: "Pulse") }) self.captureButton.layoutIfNeeded() }) } } else { // Stop recording videoCaptureOutput.stopRecording() DispatchQueue.main.async { UIView.animate(withDuration: 0.2, animations: { self.captureButtonWidthConstraint.constant = 65 self.captureButtonHeightConstraint.constant = 65 self.captureButton.layer.cornerRadius = 32.5 UIView.animate(withDuration: 0.4, animations: { self.captureBorderView.layoutIfNeeded() self.captureBorderView.layer.removeAnimation(forKey: "Pulse") }) self.captureButton.layoutIfNeeded() }) } } }
Let me know if you have any questions and clarifications. You can email me at lawgimenez@hey.com directly.
-
Create a custom camera preview using Swift
If you are looking to create your own camera on iOS, I will demonstrate how I did it on my end.
CameraPreviewView
First of all you need to subclass
UIView
and inherit its properties. We will use this Swift file for our camera preview.import UIKit import AVFoundation class CameraPreviewView: UIView { override class var layerClass: AnyClass { return AVCaptureVideoPreviewLayer.self } var videoPreviewLayer: AVCaptureVideoPreviewLayer { return layer as! AVCaptureVideoPreviewLayer } var session: AVCaptureSession? { get { return videoPreviewLayer.session } set { videoPreviewLayer.session = newValue } } }
AVCaptureVideoPreviewLayer
is a subclass ofCALayer
that you use to display video as it’s captured by an input device.Storyboard UI
Drag a
UIView
that should fill up the whole screen. Add the necessary constraints at each border. Then place aUIButton
at the lower part and also add the necessary constraints. ThisUIButton
should serve as the record button.Take note that the
UIButton
should be on top of the camera previewUIView
as not to hide it once the preview is shown.Click the
UIView
and addCameraPreviewView
as the custom class.CaptureViewController
In our ViewController above, create a file called
CaptureViewController.swift
and use it as the custom class. Then, wire the camera previewUIView
andUIButton
outlets. Thenimport AVFoundation
and add the necessary classes like the code below.import UIKit import AVFoundation import OSLog class CaptureViewController: UIViewController { @IBOutlet weak var cameraPreviewView: CameraPreviewView! @IBOutlet weak var captureButton: UIButton! // An object that manages capture activity and coordinates the flow of data from input devices to capture outputs private let captureSession = AVCaptureSession() // A capture output that records video and audio to a QuickTime movie file. private let videoCaptureOutput = AVCaptureMovieFileOutput() // A capture input that provides media from a capture device to a capture session. private var activeCaptureDeviceInput: AVCaptureDeviceInput! private let logger = Logger() private let sessionQueue = DispatchQueue(label: "Capture Session") override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) initCaptureSession() } private func initCaptureSession() { cameraPreviewView.session = captureSession // First of all ask permission from the user if AVCaptureDevice.authorizationStatus(for: .video) == .authorized { // Start capturing video startVideoSession() } else if AVCaptureDevice.authorizationStatus(for: .video) == .notDetermined { // Request permission AVCaptureDevice.requestAccess(for: .video, completionHandler: { [self] granted in if granted { logger.debug("Video capture device granted? \(granted)") startVideoSession() } }) } } private func startVideoSession() { captureSession.beginConfiguration() captureSession.sessionPreset = .hd4K3840x2160 // Setup camera let videoDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) guard let videoDeviceInput = try? AVCaptureDeviceInput(device: videoDevice!) else { return } if captureSession.canAddInput(videoDeviceInput) { captureSession.addInput(videoDeviceInput) activeCaptureDeviceInput = videoDeviceInput } // Setup microphone let audioDevice = AVCaptureDevice.default(for: .audio) guard let audioDeviceInput = try? AVCaptureDeviceInput(device: audioDevice!) else { return } if captureSession.canAddInput(audioDeviceInput) { captureSession.addInput(audioDeviceInput) } // Setup movie output if captureSession.canAddOutput(videoCaptureOutput) { captureSession.addOutput(videoCaptureOutput) } DispatchQueue.main.async { self.cameraPreviewView.videoPreviewLayer.connection?.videoOrientation = .portrait } captureSession.commitConfiguration() captureSession.startRunning() } }
That's it. Try running it on a connected device and not on the emulator.
[jetpack_subscription_form show_subscribers_total="false" button_on_newline="false" custom_font_size="16px" custom_border_radius="0" custom_border_weight="1" custom_padding="15" custom_spacing="10" submit_button_classes="" email_field_classes="" show_only_email_and_button="true"] -
Implement Chromecast on iOS using Swift
I am going to implement Google’s Chromecast on iOS using Swift.
I will be using Google Chromecast 2 device and Xcode 9.4.1.First let’s read the Google Cast documentation to get familiar with it’s implementation. After getting familiar with the Get Started section, we need to register the Google Chromecast device. There will be a one time $5 fee for Google Cast Developer Registration. After paying, we will be redirected to Google Cast SDK Developer Console
Setup for development
Assuming you already have installed the Google Home app or the Chrome browser extension, if not please do. After that we will register the Chromecast device. Click on Add New Application. For now I will choose Custom Receiver and fill up the necessary informations.
Next, under the Cast Receiver Devices click on Add New Device and enter your Chromecast’s Serial Number and your own description. After saving the device, it should indicate in the status that it is Registering. Take note that it will take several minutes to register the device.
Sender Application
Now we will create the sender application for Chromecast. Assuming you already have setup your Xcode project using Cocoapods, if not please do. Sender app refers to our mobile device or laptop which will handle the playback.
Open your
Podfile
and add the following line below and then typepod install
on your terminal.pod ‘google-cast-sdk’, ‘4.3.1’
Integrate CAF (Cast Application Framework)
Open
AppDelegate.swift
and inside the methoddidFinishLaunchingWithOptions
add the following lines belowimport UIKit import GoogleCast @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? private let appId = "0FFF55BD" func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. // Initialize Google Cast SDK let discoveryCriteria = GCKDiscoveryCriteria(applicationID: appId) let castOptions = GCKCastOptions(discoveryCriteria: discoveryCriteria) GCKCastContext.setSharedInstanceWith(castOptions) GCKLogger.sharedInstance().delegate = self return true } extension AppDelegate: GCKLoggerDelegate { func logMessage(_ message: String, at level: GCKLoggerLevel, fromFunction function: String, location: String) { print("Message from Chromecast = \(message)") } }
Take note of the
appId
variable, that is the Application ID when you registered your application in the Google Cast SDK Developer Console.Let's add a Cast button in our navigation bar. Open
ViewController.swift
and add the following code block.import UIKit import GoogleCast class HomeViewController: UIViewController { @IBOutlet weak var navItem: UINavigationItem! override func viewDidLoad() { super.viewDidLoad() let castButton = GCKUICastButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24)) castButton.tintColor = UIColor.red let castBarButtonItem = UIBarButtonItem(customView: castButton) navigationItem.rightBarButtonItem = castBarButtonItem } }
Build and run. It should have a red Chromecast button appear on the right side of the navigation bar.
Tapping on the Cast button should display a ViewController that lists the Chromecast devices that were found.
If at first the Cast button will not display on the navigation bar, try to turn on your Chromecast device. Based on my testing, the Cast button will not appear if there are no Chromecast devices found.I used the value
https://lawgimenez.me
as the value for Receiver Application URL. When everything is successful, the Chromecast should be able to cast my website to my TV. When I tap on Gimenez's Room TV, it should display the contents of my URL. I both tested this implementation on both real device and emulator. -
Implementing the expandable cell in iOS UITableView
I’m going to implement below on how to create the expandable UITableView cell for example on iOS stock calendar.
From scratch, create a new project on Xcode. In your main.storyboard remove the default ViewController (delete also the extra unused ViewController.swift file in the project explorer) and drag a new TableViewController.
Now that we have our TableViewController set on the storyboard. Let’s create a new Swift file, and call it for example FormTableViewController.swift. And set is as the custom class in your TableViewController.
Now we are all set. We are gonna change the TableView’s Content from Dynamic Prototypes to Static Cells.
After setting it to Static Cells, your TableView will have a default of 3 static cells created.
For this demo it won’t matter how many cells we are going to use.
The secret to this is the proper measurement of a cell. Click on the first cell and change the row height to 250. Our plan is, inside the cell we will have 2 views. The Label which has a height of 50 and DatePicker with a height of 200. A total of 250. On the storyboard, drag a Label and DatePicker on the first cell, setup its constraints and embed our TableViewController with a NavigationController, it should look like the image below.
Now let’s start coding. Inside your FormTableViewController you need to override two functions, namely TableView’s didSelectRowAt and heightForRowAt.
import UIKit class FormTableViewController: UITableViewController { private var dateCellExpanded: Bool = false override func viewDidLoad() { super.viewDidLoad() // For removing the extra empty spaces of TableView below tableView.tableFooterView = UIView() } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.row == 0 { if dateCellExpanded { dateCellExpanded = false } else { dateCellExpanded = true } tableView.beginUpdates() tableView.endUpdates() } } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { if indexPath.row == 0 { if dateCellExpanded { return 250 } else { return 50 } } return 50 } }
I’m going to explain shortly about the code. The variable dateCellExpanded is a flag to determine if the current cell selected has been expanded or not. Then, after always call tableView.beginUpdates() and tableView.endUpdates() and it will trigger the delegate function of TableView heightForRowAt to determine what height value should we return or not. Yep, it is that simple. Click RUN!
Hopefully this demo should give you an idea on how to implement it on your app.
The example project can be found at https://github.com/lawgimenez/expandable-cell-test