• SwiftUI Tap Anywhere

    Today I learned how to implement tap anywhere to dismiss the keyboard on SwiftUI.

    In your parent stack, either VStack or HStack, 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 and NavigationLink inside a List. 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 shiny NavigationStack. And use .navigationDestination() instead of NavigationLink.

     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 the View 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 to Color.yellow. Then you will have to add the modifier background to every Text() 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 most UILabel is for the username and lastly at the bottom is the UILabel 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 and estimatedRowHeight to UITableView.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.

    [www.loom.com/share/9e5...](https://www.loom.com/share/9e5bf20a80eb4a2bac0fb4d8e71f957f)
  • 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 a UIView.

  • 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 of CALayer 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 a UIButton at the lower part and also add the necessary constraints. This UIButton should serve as the record button.

    Take note that the UIButton should be on top of the camera preview UIView as not to hide it once the preview is shown.

    Click the UIView and add CameraPreviewView 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 preview UIView and UIButton outlets. Then import 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 type pod install on your terminal.
    pod ‘google-cast-sdk’, ‘4.3.1’

    Integrate CAF (Cast Application Framework)

    Open AppDelegate.swift and inside the method didFinishLaunchingWithOptions add the following lines below

    import 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.swiftand 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.

    IMG_0714
  • 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.

    1-K0Xsn4n9Lx2lqpUDNO-xXA

    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.

    1-ti0Kq9gWuhSdxOPXvRjDDw

    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.

    1-XPDBMIMfwL7hBdvehkUnjQ
    1-366I_Aw3939Z5uXtdVX0fg

    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.

    1-VOvLjMRH2f8Gyu7LmfFvYA

    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!

    1-_TMKiaY6yEn4iGw2w_vdvQ

    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