2022 – Code and Cloud

Well, made it thru to another year. 2022 seems to be starting with the same hope and courage of 2021, let’s hope it ends on a better note.

From Apple’s Xcode Cloud Website

I’ve been working on a Card Tracking app for a few years. It started as a project to hope me learn CoreData, I then pivoted to learn SwiftUI, and finally, I have pivoted again to use it to learn about Xcode Cloud. If you don’t know what that is, I highly recommend you check out the sessions from 2021’s WWDC.

Basically, Xcode Cloud is Apple’s CI/CD service for Xcode. (CI/CD is continuous integration / continuous delivery). This type of tool chain is used in many agile development shops to delivery new functions to customers as quickly as possible. The basic idea is, whenever new code is checked into your source repository the system will build, test, and delivery it as far as you define. In my case I would like to deliver a new build of my card tracking app to testers via TestFlight whenever I deliver new code.

This process is pretty easy, assuming you correctly define your build process. Currently I am able to have the system automatically build both my iOS and macOS apps and then attempt to post them to TestFlight. I say “Attempt” since for some reason the technique I use to auto-increment my builds is not being recognized. Xcode Cloud uses it’s own build number (CI_BUILD_NUMBER), and I can’t find a way to override it with the value I generate from my info.plist. It seems the only I can get this to work is to switch using a new release number; which would then allow for Xcode Cloud to take over the build number.

SwiftUI Fun and Bugs

Well I fixed the bug! The issue was the sequence of the code sequence between the ContactPicker and VStack within the Navigation View. I hadn’t realized that this was issue (Obviously). Here’s a video of the real way it should work.

As you can see the UI is now working as you would expect.

//
//  AddNewRecipientView.swift
//  Card Tracker
//
//  Created by Michael Rowe on 1/1/21.
//  Copyright © 2021 Michael Rowe. All rights reserved.
//

import SwiftUI
import SwiftUIKit
import ContactsUI
import Contacts
import CoreData

/* Thanks to @rlong405 and @nigelgee for initial guidance in getting past the early issues. */

struct AddNewRecipientView: View {
    @Environment(\.managedObjectContext) var moc
    @Environment(\.presentationMode) var presentationMode

    @State private var lastName: String = ""
    @State private var firstName: String = ""
    @State private var addressLine1: String = ""
    @State private var addressLine2: String = ""
    @State private var city: String = ""
    @State private var state: String = ""
    @State private var zip: String = ""
    @State private var country: String = ""

    @State var showPicker = false

    init() {
        let navBarApperance = UINavigationBarAppearance()
        navBarApperance.largeTitleTextAttributes = [
            .foregroundColor: UIColor.systemGreen,
            .font: UIFont(name: "ArialRoundedMTBold", size: 35)!]
        navBarApperance.titleTextAttributes = [
            .foregroundColor: UIColor.systemGreen,
            .font: UIFont(name: "ArialRoundedMTBold", size: 20)!]
        UINavigationBar.appearance().standardAppearance = navBarApperance
        UINavigationBar.appearance().scrollEdgeAppearance = navBarApperance
        UINavigationBar.appearance().compactAppearance = navBarApperance
    }

    var body: some View {
        NavigationView {
            GeometryReader { geomtry in
                ContactPicker(showPicker: $showPicker, onSelectContact: {contact in
                    firstName = contact.givenName
                    lastName = contact.familyName
                    if contact.postalAddresses.count > 0 {
                        if let addressString = (
                            ((contact.postalAddresses[0] as AnyObject).value(forKey: "labelValuePair")
                             as AnyObject).value(forKey: "value"))
                            as? CNPostalAddress {
                            // swiftlint:disable:next line_length
                            let mailAddress = CNPostalAddressFormatter.string(from: addressString, style: .mailingAddress)
                            addressLine1 = "\(addressString.street)"
                            addressLine2 = ""
                            city = "\(addressString.city)"
                            state = "\(addressString.state)"
                            zip = "\(addressString.postalCode)"
                            country = "\(addressString.country)"
                            print("Mail address is \n\(mailAddress)")
                        }
                    } else {
                        addressLine1 = "No Address Provided"
                        addressLine2 = ""
                        city = ""
                        state = ""
                        zip = ""
                        country = ""
                        print("No Address Provided")
                    }
                    self.showPicker.toggle()
                }, onCancel: nil)
                VStack {
                    Text("")
                    HStack {
                        VStack(alignment: .leading) {
                            TextField("First Name", text: $firstName)
                                .customTextField()
                        }
                        VStack(alignment: .leading) {
                            TextField("Last Name", text: $lastName)
                                .customTextField()
                        }
                    }
                    TextField("Address Line 1", text: $addressLine1)
                        .customTextField()
                    TextField("Address Line 2", text: $addressLine2)
                        .customTextField()
                    HStack {
                        TextField("City", text: $city)
                            .customTextField()
                            .frame(width: geomtry.size.width * 0.48)
                        Spacer()
                        TextField("ST", text: $state)
                            .customTextField()
                            .frame(width: geomtry.size.width * 0.18)
                        Spacer()
                        TextField("Zip", text: $zip)
                            .customTextField()
                            .frame(width: geomtry.size.width * 0.28)
                    }
                    TextField("Country", text: $country)
                        .customTextField()
                    Spacer()
                }
            }
            .padding([.leading, .trailing], 10 )
            .navigationTitle("Recipient")
            .navigationBarItems(trailing:
                                    HStack {
                Button(action: {
                    let contactsPermsissions = checkContactsPermissions()
                    if contactsPermsissions == true {
                        self.showPicker.toggle()
                    }
                }, label: {
                    Image(systemName: "person.crop.circle.fill")
                        .font(.largeTitle)
                        .foregroundColor(.green)
                })
                Button(action: {
                    saveRecipient()
                    self.presentationMode.wrappedValue.dismiss()
                }, label: {
                    Image(systemName: "square.and.arrow.down")
                        .font(.largeTitle)
                        .foregroundColor(.green)
                })
                Button(action: {
                    self.presentationMode.wrappedValue.dismiss()
                }, label: {
                    Image(systemName: "chevron.down.circle.fill")
                        .font(.largeTitle)
                        .foregroundColor(.green)
                })
            }
            )
        }
    }

    func saveRecipient() {
        print("Saving...")
        if firstName != "" {
            let recipient = Recipient(context: self.moc)
            recipient.firstName = firstName
            recipient.lastName = lastName
            recipient.addressLine1 = addressLine1.capitalized(with: NSLocale.current)
            recipient.addressLine2 = addressLine2.capitalized(with: NSLocale.current)
            recipient.state = state.uppercased()
            recipient.city = city.capitalized(with: NSLocale.current)
            recipient.zip = zip
            recipient.country = country.capitalized(with: NSLocale.current)
        }
        do {
            try moc.save()
        } catch let error as NSError {
            print("Save error: \(error), \(error.userInfo)")
        }
    }

    func checkContactsPermissions() -> Bool {
        let authStatus = CNContactStore.authorizationStatus(for: .contacts)
        switch authStatus {
        case .restricted:
            print("User cannot grant premission, e.g. parental controls are in force.")
            return false
        case .denied:
            print("User has denided permissions")
            // add a popup to say you have denied permissions
            return false
        case .notDetermined:
            print("you need to request authorization via the API now")
        case .authorized:
            print("already authorized")
        @unknown default:
            print("unknown error")
            return false
        }
        let store = CNContactStore()
        if authStatus == .notDetermined {
            store.requestAccess(for: .contacts) {success, error in
                if !success {
                    print("Not authorized to access contacts. Error = \(String(describing: error))")
                    exit(1)
                }
                print("Authorized")
            }
        }
        return true
    }
}

struct AddNewRecipientView_Previews: PreviewProvider {
    static var previews: some View {
        AddNewRecipientView()
            .environment(\.managedObjectContext, PersistentCloudKitContainer.persistentContainer.viewContext)
    }
}

Update on Holiday Card Tracker

A few years back I started an app that I was thinking of calling Christmas card tracker. The goal of the app was to learn Core Data. I spent time at WWDC working with a fellow attendee to fix problems I was having with Autolayout constraints, and got an early version working enough to where I could start capturing the cards my wife and I send out each year.

I then ran out of time and put it on the back burner. Along comes SwiftUI and I re-wrote the app from scratch, fixing a ton of problems, and along the way making it available for macOS, iPadOS and iOS, I even toyed with a watchOS version. It took me a few years of sporadic work, as I did a rewrite of my app Wasted Time, so that it could run on macOS and tvOS.

Well I am very close to releasing version 1.0, but have run into a very frustrating bug. When I go to add a new recipient of a card I give the user the option of searching their contacts list, or adding a new entry in the app. The Mac version of the screen can be seen here:

Add Recipient Screen.

As you can see, their are three icons. The first is save, the second is search (your contacts) and the the third is close the entry without a save. The problem is, you don’t see a cursor in the first input field – “First Name”. You can use a mouse or the tab key to move the cursor into the field. You can type and tab between fields; however, on the iPad or iPhone, you cannot touch the screen to start entering in the field. You actually can’t enter the cursor into any fields, without a mouse or keyboard.

I am hoping to resolve this issue, but I’ve been banging my head for some time. I’ve tried posting on Stack Overflow, and all I got was a standard (you’ve posted too much code) statement. I posted in two different Swift developer community slacks. And so far I am still stuck. I’ve tried setting the first responder, and that didn’t work. My next idea will be to remove the Geometry Reader that I use to size the City, State, Zip entries.

Will post when I figure this out.

Wasted Time for macOS updated

I just got notified that Wasted Time for macOS has made it thru the App Store review process. That means both the iOS and macOS versions have now been updated to support multiple languages. I am really excited by this and hope to add more languages as I go forward.

Currently, the app supports – German, Brazilian Portuguese, Spanish, Chinese (Hong Kong), Chinese (Simplified) and English. I am still experiencing an upload problem with the tvOS and watchOS versions.. but they will come soon.

App Store rejection

Well, Apple seems to have turned the screws tighter on this build of Wasted Time for the Mac. While I’ve had this out for a while, they noticed that if you closed the main window, there was no way to get it back. So the suggestion is to either add a Cmd-N (for new window) or have it completely terminate the app. I think I will make it terminate the app completely.

Localization and Internationalization

I’ve not had too much time to be able to work on my apps lately, even though I’ve been using my Holiday card tracker app a lot lately. It can be frustrating not to have the time to really dedicate learning new capabilities and APIs. On top of that, the paid (day) job has gotten really really busy for all the right reasons. So I shouldn’t complain.

Having said that I am very excited because I have amazing friend, who speak different languages, and I am ready to upload a new version of Wasted Time in multiple languages, with more languages to come!

I was hoping to make it available for watch and Apple TV too, but I am having some issues with Xcode. On the Apple TV it seems to have an issue with my graphics, which haven’t changed (so I am confused by that), and on the Apple Watch there are issues with the WKExtensionDelegate that I am working to figure out with Apple.

At least I will be able to help more people around the world be more productive!

Converting Videos

Over the years, I have converted almost my entire music library to digital (from LPs, 45RPMs, CDs, cassette tapes, and even a few Reel-to-Reel tapes). I have also taken my entire DVD and Blu-ray library and created local backup copies to make it easier to binge watch some of my favorite old movies and TV shows.

The tools for these efforts are simple and inexpensive. I use VinylStudio for Audio, and a combination of HandBrake, MakeMKV, and iFlicks 3 for Video.

For work; however, I am trying to convert a bunch of older educational videos and demos to make them consumable for team member. Converting WMV to MP4 is easy with tools like MacX Video Converter Pro, but I can’t seem to get an easy way to convert .arf files (from Webex) to MP4 files. Evidently the enterprise Webex account I have, won’t let me log into WebEx’s servers to use their conversion tool.

I keep looking on-line for tools, but can’t find any. Any ideas?

API’s and Open Development

Bunny
A Monday Morning Bunny

I rarely write posts about my day job on this blog, the focus of this blog is to address my hobbies and passions.  When those intersect with my day job I will try and post a few thoughts.  Having said that, I recently took a new job focusing on improving the API strategy of IBM’s Engineering Lifecycle Management offering (ELM).  

IBM ELM is a tool suite utilized by the world’s most successful companies which design, develop and deliver vehicles of all kinds (Planes, Trains, and Automobiles – and more)!  The suite of capabilities  was built on the idea of open development.  The team has been developing this publicly for well over a decade, and has built an incredible community at jazz.net

The basic, underlying APIs have been exposed via a standard called OSLC (Open Services for Lifecycle Collaboration).  This OSLC standard is is being maintained and enhanced via Open Oasis. 

One of the coolest things about OSLC based APIs is that they are highly discoverable at runtime when working on building integration between systems.  In ELM, we expose many services via a “RootServices” document on each server.   By parsing this document, APIs (or services) are exposed, and their definitions are exposed.  

I’ve been working on a example of how to parse this via a set of Postman examples to help new developers understand this technology.  This example will show, how to parse a RootServices document, perform a simple OAuth1.0a authentication, and then expose the definition of a specific API and the data that must be provided to create a system artifact.  Right now my example shows how to create a Automated Test Adapter.

I hope to do some cleanup of my example over the next few weeks and will post it here; however, I would like to understand what other types of open API documentation people would like to see.

The new toys!

Not playing this year

I try to upgrade my iPhone, Apple Watch, and iPads every other year (it get’s really expensive otherwise). This year is an off year, as I picked up the iPhone 12 Pro Max last year. I am not going to second guess people who upgrade every year. I think if you can afford it and it brings you joy, then you should go ahead and do it. However, I do follow the blogs, podcasts, and social feeds of many Apple fans to see the reaction each year to see if I should break my pattern.

I have only ever broken the pattern once once on the iPad (From the original to the 2nd gen) and once on the Apple Watch (From the series 1 to the series 2). As for the iPhone, I have broken it twice from the the original iPhone to the iPhone 3g, and from the iPhone 6 to the 6Plus.

What made me do it, looking back I can’t remember, but at the time the reasons were crystal clear. There was some compelling new sensor, or camera feature, that made it critical that I get that new toy!

This year, the only feature that would be compelling for the iPhone 12 Pro Max would be the battery improvement. 2.5 hours more battery is amazing! Of course, not having traveled in 18 months, it is not a feature I need. I end the day right now with between 60-70% of battery on my iPhone. I am just not using it as much as I used to. I think I have reached peek Apple toys in my life.

What would have made me open my wallet this year would have been a AR glasses developer kit. I would have dropped serious money to receive that, but at last it was not to be. Perhaps 2022 will be the year of Apple glasses!

Summer 2021 WWDC Impacts

For the last 12 years or so, I’ve really enjoyed playing with the various beta’s of Apple’s operating systems. Discovering new features, and updating my apps each summer have been a great way to keep my technological skills current, while scratching the itch to write code. I’ve had good summers and bad summers, based on the stability of the beta’s. I have had to rebuild a machine from scratch while at WWDC, and I’ve had to send Apple watches back to Apple to get recovered from bad updates. As such, you can imagine how pleased I was that this summer’s betas have been very stable on my various devices.

The only problem with the stability has been that Apple has slowly been removing many of the features that were exciting to me. Apple also never provided Beta’s this summer with the most exciting features they previewed at WWDC. To me these two features were – Writing SwiftUI apps in SwiftPlaygrounds, and dynamically controlling your iPad and Mac by sliding to the edge of the screen.

I really hope that Apple talks to these features at Tuesday’s iPhone event!

Btw, you may have seen that I posted two additional posts today.. both of them had somehow gotten stuck in drafts late last year and earlier this year.