Andy's iOS Journal

Day-18-geometryReader

  • Apr 19, 2021
  • iOS Foundations Module 4 Lesson 4

GeometryReader

Geometry reader is a container that acts similar to a ZStack but can provide position information.

GeometryReader { geo in
  Rectangle()
    .frame(width: geo.size.width)
    geo.frame(in: .local).minX  // get the location in local or global positioning
}

Shapes

  • Circle()
  • Rectangle()

Gestures

Modifier that can be applied to an object like the Circle or Rectangle or container.

.onTapGesture {
  // code to execute onTap gesture
}

Positioning

  • .padding
  • .offSet

Day-19-environmentObject

  • Apr 23, 2021
  • iOS Foundations Module 4 Lesson 5,6

The environmentObject modifier can be attached to a view object to supply data to all downstream views

So at the top level view code may look like this with RecipeModel() containing the data we want to use in the downstream views.

TabView {

}
.environmentObject(RecipeModel())

Each downstream view will need to have a line of code that defines a variable that the environmentObject data can be placed.

@EnvironmentObject var model:RecipeModel

Something to keep in mind: I read that environmentObjects are kept in memory for the lifetime of the app, which means, we want to be careful the type can quantity of data we store in an environmentObject.

This website had a really great explanation between @State, @Binding @EnvironmentObject and when to use them.

Day-20-challenge

  • Apr 24, 2021
  • iOS Foundations Module 4 Lesson 6 Challenge

Spoiler Warning lots of hints on how I solved this challenge below.


Create an app with 2 tabs that displays person information.

When creating a class, either the class has to have a default init function

init() {
  //code here that will create default values for all variables in the class
}

or

All the elements in the class should be optional except say the Id.

class Person: Identifiable {

    var id = UUID()
    var name:String?
    var address:String?
    var company:String?
    var experience:Int?

}

What we want here though is actually a struct not a class as it makes it easier to create a people Array of Persons.

Error: invalid mode 'kCFRunLoopCommonModes'

StackOverflow suggests this is just a warning that Apple is working on and can be safely ignored.

toggles object

We can only pass one object to the environmentObject modifier that we use on the TabView. So we need to create an object that can hold the for bool variables.

class toggles: ObservableObject {
    @Published var showName = true
    @Published var showAddress = true
    @Published var showCompany = false
    @Published var showExperience = true
}

Those variables need to be @Published or else the toggles won’t update the class variables and the class needs to be an ObservableObject.

I really liked how this challenged forced me to combine all the elements we have learned so far.

  • TabView
    • View Instances for FirstTab and Second Tab
  • environmentObject, @EnvironmentObject
  • ObservableObject, @Published, @State
  • Structs, arrays,
  • Lists,
  • If statements
  • Toggle
  • MVVM model

Day-21-FeatureView

  • iOS Foundations Module 4 Lesson 7 Challenge
  • Apr 25, 2021

This lesson went super fast. I had to pause it several times to catch up. I also had to find a mistake as to why my .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always)) was not working properly. The dots weren’t showing up. Turned out I had placed that modifier in the wrong location. It needed to be attached to the } of TabView and I had it one curly bracket up. I have started to label all of my end brackets like this.

VStack {
  HStack {
    GeometryReader { geo in
                TabView {

                }//TabView
    } //GeometryReader
  }//Hstack
}// Vstack

At least for now this seems to make it easier for me to see the code clearly.

Things I learned

Use TabView to make swipeable cards

TabView {
  //code
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))

How to add a Shadow

Rectangle()
.shadow(color: Color(.sRGB, red: 0, green: 0, blue: 0, opacity: 0.5), radius: 10,x: -5, y: 5)

Review of VStack parameters

Very useful for modifying space

VStack(alignment: .leading, spacing: 0)

Day 21

Took a break and tried to make a color slider. I think it came out well.

image

Day-22-StockPickerChallenge

  • Stock Picker Challenge
  • Apr 30, 2021

While I am enjoying the tutorials, I am itching to try out my skills on a project without step by step instructions. I could start on some of my own ideas but I know they require core data and some other skills i haven’t gotten to yet. So instead I am going to dive into the Stock Picker Challenge this month with the rest of the CodeCrew group.

Basic Requirements for the Challenge:

  • Get stock data from an API
  • Display a list of stocks, with respective prices and changes in dollar amount.
  • Be able to edit the same list of stocks, and have the option to remove and add stocks by ticker symbol.

I am going to aim for this basic requirement and add in a few extra features that I am interested in learning how to do.

Since this challenge starts tomorrow, I want to make sure I can utilize the API properly

The API

Per the recommendation in the challenge, I am goign to use this Stock API

I tried using the apikey=demo and all that this key can do is apparently pull down data for stock ticker symbol AAPL. If you try to do a batch pull or any other symbol you get the following error.

{"Error Message" : "Invalid API KEY. Please retry or visit our documentation to create one FREE https://financialmodelingprep.com/developer/docs"}

The only way to get around this is to sign up for a free account. They don’t ask you for a credit card and it gives you 250 requests per day.

Sign up for a free API key here

After I signed up it said you had to verify your email but I never got an email so I signed in through my google account and was able to get to the dashboard they provide which has the api key. Once I had that everything worked as expected for single stock picker symbols. We can test this directly in the web browser and get a couple stocks to populate a json of starter stocks to explore in the App.

https://financialmodelingprep.com/api/v3/quote/GOOG?apikey=myNewShinyAPIKey

Turns out that for batch submissions you need to upgrade for the subscription package. I am not going to be doing this as this is just for fun so we are limited to single tock ticker api requests.

https://financialmodelingprep.com/api/v3/quote/GE,GOOG?apikey=myNewShinyAPIKey
{"Error Message" : "Special Endpoint : this endpoint is only for premium members please visit our subscription page to upgrade your plan at https://financialmodelingprep.com/developer/docs/pricing"}

NOTE: you have to replace myNewShinyAPIKey with the apikey from your account or it won’t work.

So we now can get one stock at a time.

GE

https://financialmodelingprep.com/api/v3/quote/GE?apikey=myNewShinyAPIKey
[ {
  "symbol" : "GE",
  "name" : "General Electric Company",
  "price" : 13.12000000,
  "changesPercentage" : -0.68000000,
  "change" : -0.09000000,
  "dayLow" : 13.08000000,
  "dayHigh" : 13.35000000,
  "yearHigh" : 14.42000000,
  "yearLow" : 5.48000000,
  "marketCap" : 115175759872.00000000,
  "priceAvg50" : 13.24200000,
  "priceAvg200" : 11.16021700,
  "volume" : 48760561,
  "avgVolume" : 76976082,
  "exchange" : "NYSE",
  "open" : 13.16000000,
  "previousClose" : 13.21000000,
  "eps" : -0.45300000,
  "pe" : null,
  "earningsAnnouncement" : "2021-04-27T12:30:00.000+0000",
  "sharesOutstanding" : 8778640234,
  "timestamp" : 1619825257
} ]

AAPL

https://financialmodelingprep.com/api/v3/quote/AAPL?apikey=myNewShinyAPIKey
[ {
  "symbol" : "AAPL",
  "name" : "Apple Inc.",
  "price" : 131.46000000,
  "changesPercentage" : -1.51000000,
  "change" : -2.02000000,
  "dayLow" : 131.06500000,
  "dayHigh" : 133.56000000,
  "yearHigh" : 145.09000000,
  "yearLow" : 71.46250000,
  "marketCap" : 2206963859456.00000000,
  "priceAvg50" : 127.59143000,
  "priceAvg200" : 125.78232000,
  "volume" : 105917721,
  "avgVolume" : 100930927,
  "exchange" : "NASDAQ",
  "open" : 131.78000000,
  "previousClose" : 133.48000000,
  "eps" : 4.44900000,
  "pe" : 29.54821600,
  "earningsAnnouncement" : "2021-04-28T16:30:00.000+0000",
  "sharesOutstanding" : 16788101776,
  "timestamp" : 1619825031
} ]

GOOG

https://financialmodelingprep.com/api/v3/quote/GOOG?apikey=myNewShinyAPIKey

[ {
  "symbol" : "GOOG",
  "name" : "Alphabet Inc.",
  "price" : 2410.12000000,
  "changesPercentage" : -0.81000000,
  "change" : -19.77000000,
  "dayLow" : 2402.29000000,
  "dayHigh" : 2427.14000000,
  "yearHigh" : 2452.37800000,
  "yearLow" : 1299.00000000,
  "marketCap" : 1602411692032.00000000,
  "priceAvg50" : 2187.98320000,
  "priceAvg200" : 1915.57230000,
  "volume" : 1881168,
  "avgVolume" : 1499530,
  "exchange" : "NASDAQ",
  "open" : 2404.49000000,
  "previousClose" : 2429.89000000,
  "eps" : 75.04000000,
  "pe" : 32.11780500,
  "earningsAnnouncement" : null,
  "sharesOutstanding" : 664868012,
  "timestamp" : 1619825446
} ]

Good luck everyone! I am looking forward to everyone’s answers to this App Challenge.

Day 23-StockPickerD2

  • Day2 of the StockPickerChallenge
  • May 1, 2021

Steps I have taken so far

  1. Use the recipeList app as a template
  2. Set up groups:
    a) MVVM
    b) Data
    c) Services
    d) Extensions
  3. Copy over from recipeList these files
    a) RecipeModel.swift → StockModel.swift
    b) DataServices.swift
    c) recipes.json → stocks.json
    d) add json data from the three stocks above
    e) RecipeListView → StockListView
  4. Create Stock.swift
    a) Be careful not to mistype any of the names in the json or it won’t work!
  5. Modify the DataServices.swift to work with the new stocks.json file
  6. Create Views
    a) contentView → StockListView
    b) StockInfoView

I decided to keep using the @EnvironmentObject var model:StockModel so it is important to modify the App.swift file to include the .environmentObject(StockModel) This took me a while to remember. It kept complaining about a type error.

@main
struct StockTrackerChallengeApp: App {
    var body: some Scene {
        WindowGroup {
            StockListView().environmentObject(StockModel())

        }
    }
}

This really just gets us to the point where we can start building the interface using the data from the json file. Once we are happy with the interface we can go back and rewrite the DataServices.swift to read from an Array of our choosing and then save that array to Defaults. Ideally I would save it to core data but I haven’t learned that yet. :slight_smile:

Trailing Zeros

Those trailing zeros are really ugly. I found this extension on stackoverflow that extends double and add a function that will remove the extra zeros.

import Foundation

extension Double {
    func removeZerosFromEnd() -> String {
        let formatter = NumberFormatter()
        let number = NSNumber(value: self)
        formatter.minimumFractionDigits = 0
        formatter.maximumFractionDigits = 16 //maximum digits in Double after dot (maximum precision)
        return String(formatter.string(from: number) ?? "")
    }
}

With this you can add Text elements to a view like this

Text(" $ \((r.price).removeZerosFromEnd())")

Conditional modifiers!

Learned another new thing in swift.

.foregroundColor((r.change < 0) ? .red : .green)

TODO

  • add a + symbol to link to a function to add a new stock.
  • add a function that will download a new stock
    • append added stock to stock array.
  • add a popup to specify which ticker symbol to download
  • delete a stock using a touch gesture drag to the right
  • save array for future sessions
1 Like

Day-24-stockPickerD3

  • Day 3 of the StockPickerChallenge
  • May 2, 2021

add a + symbol to link to a function to add a new stock.

.navigationBarTitle("Your Stocks")
    .toolbar {
            ToolbarItemGroup(placement: .navigationBarTrailing) {
                Button("+") {
                  self.showingAlert.toggle()
                }
            }//ToolBargroup
    }//toolbar

add a function that will download a new stock

This was considerably more challenging as it involves the Apps architecture. It would make sense that this function should be part of the DataServices.swift file but that is currently decoding my local json (in app) file with a few example Stocks. And the StockModel.swift then grabs that data and sets it to an array of Stocks.

Perhaps I could still put this function in dataServices but I have it currently in StockModel so that I can just call it directly from model in my views.

func getRemoteData(ticker: String)  {
        //let ticker = "OTTR"

        // Set up url with ticker variable
        let url = URL(string: "https://financialmodelingprep.com/api/v3/quote/\(ticker)?apikey=0f5eedfec854e23eb12dc71fbaa4dab7")

        //create the url request
        var request = URLRequest(url: url!)

        // this code is from the example they provided from the API and returns data.
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        let task = URLSession.shared.dataTask(with: url!) { data, response, error in
            guard error == nil else {
                print(error!)
                return
            }//Guard
            guard let data = data else {
                print("Data is empty")
                return
            }//Guard

          // process the data from the url request as we would from file and append to [Stocks] array
            do {
                // decode data with a JSONDecoder
               let decoder = JSONDecoder()
               //
                //pass in type
                do {
                    let stockData = try decoder.decode([Stock].self, from: data)
                    // add unique IDs
                        for r in stockData {
                            r.id = UUID()
                            print(r.name)
                        }//for

                        // Return the stocks
                    DispatchQueue.main.async { // required to run on the main thread to execute this.
                        self.stocks = self.stocks + stockData
                    }
                } catch {
                    print(error)
                }//do-catch


        }

        }//task
        task.resume()


    }

I spent a lot of time realizing that the URLSession.shared.dataTask already gives us a data variable that we can use directly in the JSONdecoder. I kept trying to convert the json into a Data object, which apparently I already had. :confused:

append added stock to stock array.

Turns out if you want to modify an environmentObject you have to run it on the main thread so I had to wrap this modification in a DispatchQueue.main.async block

DispatchQueue.main.async { // required to run on the main thread to execute this.
     self.stocks = self.stocks + stockData
 }

add a popup to specify which ticker symbol to download

I really wanted to include an alert and @Chris_Parker has a great solution.

Chris Parker’s swiftUI alert

I went ahead and just made an if else statement that either shows the popup or the list of stocks.

TODO

  • Why is the textEntered not updating unless done twice?
    • I needed to add the model environmentObject to the CustomAlert View and then run the model.getRemote function from there and it updated as expected.
  • delete a stock using a touch gesture drag to the right

Day-25-StockPickerD4

  • May 3, 2021
  • Day 4 of the StockPickerChallenge

delete a stock using a touch gesture drag to the left

I learned so much while implementing this. It is super easy to make a editable list and add the option to move items around in a list. SwiftUI pretty much gives this to us for free.

The following modifiers can be attached to a ForEach block

  • .onDelete(perform: deleteItems)
  • .onMove(perform: move)

My List didn’t have a ForEach block so I had to change

List(model.stocks) { r in

}

to

List {
                ForEach(model.stocks, id: \.self) { r in  //requies that model.stocks conforms to Hashable

                }//ForEach
                .onDelete(perform: deleteItems)
                .onMove(perform: moveItem)

      }

This caused an error that the Stock.swift Model had to conform to Hashable for this to work. And it turns out this can’t be done automagically if the Stock Object is a class instead of a struct.

In order to add Hashable to Stock.swift I had to add the following two functions

  • ==
  • hash
class Stock: Identifiable, Decodable, Hashable {
    static func == (lhs: Stock, rhs: Stock) -> Bool {
        return lhs.id == rhs.id

    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }

    // will decode what we want to decode
    var id:UUID?
    var symbol:String
    var name:String
    var price:Double
    var changesPercentage:Double
    var change:Double
    var dayLow:Double
    var dayHigh:Double
    var yearLow:Double
    var yearHigh:Double
    var open:Double
    var timestamp:Int

}

With that everything just automagically works!

Adding the red/green rectangle

ZStack {
              Rectangle()
                  .foregroundColor((r.change < 0) ? .red : .green)
                  .frame(width: 70, height: 20, alignment: .trailing)
              Text(" \(r.change.removeZerosFromEnd())")
                  .foregroundColor(.white)
                  .bold()

}

This pretty much finishes the basic requirements for the challenge but there are so many missing features yet. I have decided to model my app after the Stocks app that we all have on our phones as default.

Additional Features

  • refresh button pull down if possible
  • Chart of last day or week or couple days of change.
1 Like

Day-26-StockPickerD4

  • May 6, 2021
  • Day 5 of the StockPickerChallenge

Refresh button pull down if possible

This apparently has not been added yet and has to be coded. I tried to implement it with what I found here: how-to-making-pure-swiftui-pull-to-refresh but it doesn’t seem to be very easy to add to my code. I suspect that this will be added after WWDC so I won’t fret about it for now and make a refresh button. It was really the backend that I wanted to figure out how to do anyway.

  • How do you update each stock. If we owned the API or had the backend we could do a bulk udpate but since we don’t and have to use one stock at a time I will have to do a loop through each of the Stocks and update them one by one.

If they do add an onPull(perform: refresh) operation, it will be really easy to just put the refresh function in it later.

For the charts, I learned how to use the swift package manager

Swift Package Manager (swiftPM)

  1. click on the app name and you will have several tabs you can choose from. This is the same area where you can set the iOS Deployment Target.
  2. Click on the Swift Packages Tab
  3. Click on the + under the Packages.
  4. Enter the github repo for the package.

That’s it!

So I added a package called SwiftUICharts. It is still rudimentary but looks like they are going to be coming out with a version 2 soon. The nice thing about the line chart form this package is that it will take in an array of numbers and draw the chart for you. I have a simple mockup using an array I randomly generated. I need to add the code that pulls the day or week change in value for each stock ticker.

Once I have these two functions implemented I am going to go back to the tutorials for a bit. Though looks like I may have trouble with iOS foundations Module 5 as I had to update to XCODE 12.5 since I had updated my phone to 14.5.

2 Likes

Day-27-StockPickerD4

  • May 7, 2021
  • Day 6 of the StockPickerChallenge

For those readers paying attention you will notice that my dates aren’t consistent with the Day of my 100 days and that is because I don’t always get an opportunity to code every day. And I want this to reflect what I accomplish in 100 days that I am actually coding.

Today I tackled the refresh all stocks button.

Refresh Button

I put the refresh button in the same toolbar group as the EditButton()

For each stock in the model run the getRemoteData function for that stock.symbol

ToolbarItemGroup(placement: .navigationBarTrailing) {
        Button("Refresh") {
            for stock in model.stocks {
                model.getRemoteData(ticker: stock.symbol)
            }
            }

        EditButton()

}

Of course for that to work properly we have to modify the getRemoteData function

getRemoteData() function

  • Check to see if stock symbol is already present
  • if present replace stock data at the correct position in the stock array with new data.
  • if not present append to stock array
func getRemoteData(ticker: String)  {
        //let ticker = "OTTR"

        // Set up url with ticker variable
        let url = URL(string: "https://financialmodelingprep.com/api/v3/quote/\(ticker)?apikey=myNewShinyAPIKey")

        //create the url request
        var request = URLRequest(url: url!)
// had to add a stockSymbols varaible here
        var stockSymbols = [String]()  // store all the stock symbols

        // this code is from the example they provided from the API and returns data.
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        let task = URLSession.shared.dataTask(with: url!) { data, response, error in
            guard error == nil else {
                print(error!)
                return
            }//Guard
            guard let data = data else {
                print("Data is empty")
                return
            }//Guard

          // process the data from the url request as we would from file and append to [Stocks] array
            do {
                // decode data with a JSONDecoder
               let decoder = JSONDecoder()
               //
                //pass in type
                do {
                    let stockData = try decoder.decode([Stock].self, from: data)
                    // add unique IDs
                        for r in stockData {
                            r.id = UUID()
                            print(r.name)
                        }//for

                        // Return the stocks
///Modified this block of code
                    DispatchQueue.main.async { // required to run on the main thread to execute this.

                        for stock in self.stocks {

                            stockSymbols.append(stock.symbol)
                        }
                        if stockSymbols.contains(ticker) {  // Check to see if the symbol already exists and if so update it
                            self.stocks[stockSymbols.firstIndex(of: ticker)!] = stockData[0]
                        } else {  //otherwise add it to the stocks
                        self.stocks = self.stocks + stockData
                        }//IFELSE block

                    }//DispatchQueue
///Modified block of code above
                } catch {
                    print(error)
                }//do-catch


        }

        }//task
        task.resume()


    }

Refresh works now! :slight_smile: I will work on the chart perhaps tomorrow morning.

1 Like

its so refreshing to see you post stuff that I can actually understand lol! I got into this just a few months before @Chris2 started the SwiftUI revamp and I haven’t seen a reason to go back and learn it yet, so most of it is still greek to me lol! but this! fetching data I can read and help with… lol not that you needed help this time

Thanks @Pagasi, That makes so much sense. I was wondering why so few people were commenting on my journal. It is definitely worth learning swiftUI. I spent quite a bit of time learning UIKit and I don’t regret it in the slightest but there is so much functionality gained from swiftUI. The interface is so much easier to write and have it look polished without having to worry about constraints for a view or a particular model of phone or ipad etc.

Day-28-StockPickerD7

  • May 22, 2021
  • Day 7 of the StockPickerChallenge

I haven’t had much time over the last couple of weeks to make progress.

The current version solves the basic challenge but I wanted to download stock history and have that get incorporated in a stockInfo vew. Unforutately, there is something simple that I am missing that I can’t quite get the date: closingValue information out of the json and into a [Double] rather than it coming back as a dict or some other object I haven’t figured out what.

Either way I got out of this what I wanted, I am going back to tutorial grinding and personal projects for a while.

Day-29-SwiftUI-and-spritekit

  • Jun 9, 2021
  • SpriteKit and SwiftUI

Objective for this morning is to create the code organization for a simple SpriteKit/SwiftUI App. I am going to use the code from Paul at HWS as a starting point.

There are really 2 problems that we need to address.

  1. How do we make a spritekit scene into a view that swiftui can use.
  2. How do we organize it so that it doesn’t end up all in a single file.

The first point is pretty well solved with the tutorial mentioned above and just works.

DropBoxView.swift

import SwiftUI
import SpriteKit

struct BoxDropView: View {

    var scene: SKScene {
            let scene = BoxDropScene()
            scene.size = CGSize(width: 300, height: 400)
            scene.scaleMode = .fill
            return scene
        }

    var body: some View {
        VStack {
            Text("Box Drop Game")
                .bold()
                .scaleEffect(CGSize(width: 2.0, height: 2.0))

            SpriteView(scene: scene)
                .frame(width: 300, height: 400)
                .ignoresSafeArea()
        }

    }
}

The main points here are that we define a scene variable that is of type SKScene. Inside this SKScene is an instance of the spritekit scene (BoxDropScene).

In the tutorial, that class is in the same file but it doesn’t have to be. So the code above by itself will error because we haven’t defined the BoxDropScene class

BoxDropScene.swift

This is pretty much from the tutorial example, It makes a box that falls when you tap. I decreased the size of the boxes to make it a little more interesting and gave it a physicsBody that is just slightly larger than the box so the boxes don’t actually touch.

import SpriteKit


class BoxDropScene: SKScene {
    override func didMove(to view: SKView) {
        physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let location = touch.location(in: self)
        let box = SKSpriteNode(color: SKColor.red, size: CGSize(width: 20, height: 20))
        box.position = location
        box.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width:22, height: 22))
        addChild(box)
    }
}

Folder organization

  • View
    • MainScreen.swift
    • BoxDropview.swift
  • SpriteKitScenes
    • BoxDropScene.swift

Down the road we may want the following for score data etc.

  • Model
  • ViewModel

MainScreen.swift

For the main screen, I am using NavigationLink and NavigationView to create a text button that will redirect the user to the BoxDropView() which contains the BoxDropScene written in SpriteKit.

import SwiftUI
import SpriteKit




struct MainScreen: View {
    var body: some View {
        NavigationView {
            ScrollView {

                NavigationLink(
                    destination: BoxDropView(),
                    label: {
                        Text("Box Drop Game")
                    })//NavigationLink

            }//ScrollView
        }//NavigationView
    }//Body
}//Struct End

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        MainScreen()
    }
}

With this template, we can now design the App in SwiftUI and wrap the SpriteKit into scenes that we can display using SwiftUI’s SpriteView. We could make any number of games and put them in a list or as cards that we can swipe through.

Day-29-SwiftUI-and-spritekit

  • Jun 9, 2021
  • Geometry reader

Next up is can I incorporate geometryReader into the SpriteView so that it fills up the available space? The main issue I was having is that when rotated, the scene was causing the boxes to elongate and weren’t boxes anymore as the scene was stretched. That won’t be good to go to production.

Solution

MainScreen.swift

  1. Created a @State variable in the MainScreen.swift
@Binding var geoSize: CGSize
  1. Added an .onRecive(notificationCenter) after Navigation view that updates the geoSize binding upon a device change orientation notification.

  2. Added a function that sets the Geometry size.

private func saveGeoSize(for geo: GeometryProxy){
    let size = CGSize(width: geo.size.width, height: geo.size.height)
    self.geoSize = size
    print(self.geoSize)
}
struct MainScreen: View {
    @State var geoSize = CGSize(width: 30, height: 40)
    @State var reOrient = false

    var body: some View {
        GeometryReader { geo in
                NavigationView {
                    ScrollView {

                        NavigationLink(
                            destination: BoxDropView(geoSize: $geoSize, reOrient: $reOrient),
                            label: {
                                Text("Box Drop Game")
                            })//NavigationLink

                    }//ScrollView
                }//NavigationView
                .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
                    self.reOrient = true
                    self.saveGeoSize(for: geo)

                }
        } //GeometryReader
    }//Body

    private func saveGeoSize(for geo: GeometryProxy){
        let size = CGSize(width: geo.size.width, height: geo.size.height)
        self.geoSize = size
        print(self.geoSize)
    }
}//Struct End

BoxDropView

  1. added a @Binding
@Binding var geoSize: CGSize

  1. Updated the scene.size and SpriteView().frame variables
scene.size = CGSize(width: self.geoSize.width*0.9, height: geoSize.height*0.9)
SpriteView(scene: scene)
                                .frame(width: geo.size.width*0.9, height: geo.size.height*0.9)
                                .ignoresSafeArea()

references

Remaining issues

  • The scene resets every time the notification is activated. The binding updates which the updates the scene by creating a new BoxDropScene, which is a class so all children (boxes) get reset every time you reorient. I could just force it to be in one or the other mode but I am curious to see what other options are possible. I suppose I could also make it so upon rotation the frame doesn’t change from the original size. But both of these are not terribly satisfying.

Day-29-Shared-Instances

  • Jun 9, 2021
  • Shared instances

Shared instances appear to solve most of the issue I am having but not fully.

Solution

  1. Add a shared instance to the BoxDropScene class
class BoxDropScene: SKScene {
    static let shared = BoxDropScene()
  1. Change the instance of the BoxDropScene upon creation in BoxDropView.swift from
var scene: SKScene {

        let scene = BoxDropScene()

to

var scene: SKScene {
        let scene = BoxDropScene.shared

Remaining Issues

The children in the box can be lost when going from landscape to portrait and the State variable is in the mainScreen, which means the geoSizes (portrait or landscape) is set on that screen.

Question

Can I keep users from switching after entering the game? – Looks like this is possible though may be slightly more complicated to pull off. See SwiftUI: Force orientation on a per screen basis
for more details.

References

spritekit-transition-between-scenes-without-resetting-game

I think if I make a game I will just decide to have it in one orientation if I am going to

Day-30-LazyStacks

  • iOS Foundations Module 4 Lesson 8 LazyStacks
  • Aug 1, 2021

Wow so after almost 3 months of me exploring some side projects and life getting in the way (hernia surgery etc – all good now). I am finally getting back into the tutorials for module 4. It is a bit challenging to start off where I left off.

I had actually explored LazyStacks in one of my side projects. I listed the alphabet and had it slide horizontally rather than vertically.

Anyway, I am back, and will be plugging away at these tutorials again. Super excited to see what the database and design courses have to offer. I may skip city module and come back to it later.

The skills I really need to pick up in order to get my own ideas off the ground are

  • CoreData
  • CloudKit

I want to also look into ResultBuilder and making my own modules that I can load for reusable code.

Day-31-Picker

  • iOS Foundations Module 4 Lesson 9 Picker
  • Aug 3, 2021

I didn’t realize that the scrolling wheel that many apps use and often times adds haptics too was called Picker

@State var location = 1

Picker("location", selection: $location) {
     Text("a").tag(1)
     Text("b").tag(2)
     Text("c").tag(3)
}

I wonder if the picker can

  • Be shown horizontal instead of a vertical wheel.
  • Be changed in size so that more of the wheel can be shown,
    • A diameter parameter would be neat

If this is possible it would be possible to set it up like a LazyHStack but have tags that make it easier to jump to specific positions.

Day-32-Portions

  • iOS Foundations Module 4 Lesson 10 and 11
  • Aug 6, 2021

Technically, I started the challenge in lesson 9 but was having trouble staying motivated to do it. So I looked at the answer and continued my momentum forward. Picker is quite versatile.

Lesson 10 just covered how to calculate a portion for the recipe app. Lesson 11 started the implementation. It covered static functions in classes and also implemented a picker in the app.

Day-33-Portion-Calc

  • iOS Foundations Module 4 Lesson 12
  • Aug 13, 2021

For those that are juggling learning swift on the side while maintaining a life and a full time job. This is your reminder, that it is ok to continue to plug away at these tutorials and your own projects in the time you find or carve out. As you can see, I am not able to actively work on swift learning or my projects every day.

This lesson actually implemented the calculate the portion that was described in lesson 9/10.

One nice nugget that is in this lesson is in line if statements.

A variable can be reassigned depending on a conditional.

newvariable = (conditional statement) ? "than this" : "else this"

Examples

portion += wholePortions > 0 ? " " : ""

portion += ingredient.num == nil && ingredient.denom == nil ? "" : " "

Day-33-Featured-Details

  • iOS Foundations Module 4 Lesson 13
  • Aug 13, 2021

Chris covered a lot in this lesson. At times it seemed pretty fast and I had to pause it to get caught up. Some of that is probably because I am coming back to this project after a lot of time and am not holding it all in my head.

Some interesting tidbits:

  • A button can be placed around anything and a .sheet modifier can be used to tell it where to go.
.sheet(isPresented: $isDetailViewShowing){
      // Show recipe Detail View
      RecipeDetailView(recipe: model.recipes[index])
  }
  • When a @State variable needs to be updated the init method wont’ work but the .onAppear method can be used.
.onAppear(perform: {
            setFeaturedIndex()
        })

I can see how the .onAppear method could also be helpful when updating other variables.

A quick search revealed that there are other methods for [gestures](Apple Developer Documentation too!

Day-33-Custom-Fonts

  • iOS Foundations Module 4 Lesson 14
  • Aug 13, 2021

This was a short and useful tutorial on how to add custom fonts.

How to use custom fonts in swift

System Fonts ready to use

.font(Font.custom("Avenir Heavy",size: 24))

Done with Module 4!

Day-33-34-35-36-Core-Data

  • IosDatabases (Swift UI) Module 5

Core data is one of the libraries critical for most of the apps I want to build. I have spent the last couple of weeks working my way through Core data and now have a pretty good grasp of what it is and does. I have one more lesson to go through.

Lesson 1: Basics

Benefits

  • First Party Framework
    • will not be left behind
    • works with other Apple APIs/frameworks
  • Not 3rd party library

Drawbacks

  • Not for remote data storage
  • There is learning curve
  • Object graph persistent framework

Graph Persistent framework

Core data manages the SQL-lite database for you behind the scenes.

  • Object Graph in Memory
  • Managed Object Context
    • data management layer (handles saving loading etc)
  • Core data Persistant container

Methods

  • NSManagedObject can be serialized and stored in CoreData
  • Classes are defined as Entities
  • Properties are Attributes

Core data will automatically generate classes for you using a GUI.

  • the file that is labeled filename.xcdatamodeld
    • contains the gui for setting up managed classes (Entities).

Lesson 2

  • Define entities and attributes
  • Generate the classes in the model
  • Get a reference to the Core Data persistent container
  • Get the managed object context

Codegen property in the Entity Class property section (right side window)

  • Manual/None

    • Editor->Create NSManagedObject subclass
      • generates 2 files
        • Filename+CoreDataClass.swift
          • add methods in this file
        • Filename+CoreDataProperties.swift
          • at this time try not to add anything to this file.
  • Class definition

    • No files are generated, no class extension possible. These files will only be generated in the product
  • Category/Extension

    • if you aren’t going to add anything to the second file Filename+CoreDataProperties.swift then let XCode just generate it in the product for you. The first file will be generated for you to edit Filename+CoreDataClass.swift

How to add core data after the fact

  1. Add new file → CoreData (Data Model)
  • save with the same name as your project
  1. Add Code to Persistance.swift
  • Make sure the persistant container has the right name
  • take code from a fresh project that you added core data from the start.
  1. add Entities and Attributes

Configuring Entities:

Lesson 3

  • Aug 21, 2021
  • Added
  • Delete
  • Update
  • Save

File Prep

After I started a new project with Core data it added a bunch of code that can be commented out.

Persistence.swift

Comment out the following blocks of code as it only applies to previews as part of the example Apple provides

//    static var preview: PersistenceController = {
//        let result = PersistenceController(inMemory: true)
//        let viewContext = result.container.viewContext
//        for _ in 0..<10 {
//            let newItem = Item(context: viewContext)
//            newItem.timestamp = Date()
//        }
//        do {
//            try viewContext.save()
//        } catch {
//            // Replace this implementation with code to handle the error appropriately.
//            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
//            let nsError = error as NSError
//            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
//        }
//        return result
//    }()
//        if inMemory {
//            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
//        }

Environment variable

The environment variable at the top of ContentView.swift sets the variable for the managedObjectContext. IN our case, this variable is named viewContext. But should be placed at the top of your view hierarchy.

@Environment(\.managedObjectContext) private var viewContext

How to create a Person object

let p = Person(context: viewContext)

Fetch all Person objects and put into var people

  • put this near Environment variables
@FetchRequest(sortDescriptors: []) var people: FetchedResults<Person>

Previews in ContentView

I was able to get the preview working by changing the .preview to .shared.

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
    }
}

Save

It is important that after any action (Add, Delete, Update) that the save method is called.

do {
    try viewContext.save()
} catch  {
    // error with saving
}

Add

private func addItem() {

    let p = Person(context: viewContext)
    p.age = 20
    p.name = "Joe"

}

Delete

Where person is a Person objects from the array persons

viewContext.delete(person)

Update

You can modify the attributes in any of the objects in the array of class objects and the call save.

person.name = "Ted"

Lesson 4 Sorting and Filtering with Core Data.

  • Aug 28, 2021
  • NSSortDescriptor: sorting
  • NSPredicate: Filtering

The FetchRequest described above can be sorteded and filtered using NSSortDescriptor and NSPredicate

  • sortDescriptors: [NSSortDescriptor(key: “age”, ascending: true)] is an array so you can add more than 1 NSSortDescriptor
  • predicate: NSPredicate(format: “age > 9 && age < 16”)) can be used to filter based on attributes in the core data.
  • var people is where you are setting the fetch request.

If you have set filters you want to apply to data with no user input you can put those filters/sorts directly in the fetch request as follows.

    @FetchRequest(sortDescriptors: [NSSortDescriptor(key: "age", ascending: true)], predicate: NSPredicate(format: "age > 9 && age < 16")) var people: FetchedResults<Person>

More often however, we want to be able to give the user the ability to sort and filter. In order to do this we need to create some @State variables and update those State variables using .onchange modifiers associated with Text or Stacks.

@State var people = [Person]()
@State var filterByText = ""




VStack {
  // Textfield for filter
  TextField("Filter Text", text: $filterByText) { _ in
      // Fetch new data
      // Gets activated upon mouse insertion into text field and upon enter
      //fetchData()
  } // List of people you fetched
  List {
               ForEach(people) { person in
               }


  }
  .onChange(of: filterByText, perform: { value in
    fetchData()
  })

} // End of VStack

func fetchData() {
    //Create fetch request
    let request = Person.pfetchRequest()
    //Set  sort descriptors and predicates
    request.sortDescriptors = [NSSortDescriptor(key: "age", ascending: true)]
    // Don't need single quotes for %@
    request.predicate = NSPredicate(format: "name contains %@", filterByText)
    //Execute the fetch on the main thread so it updates right away
    DispatchQueue.main.async {
        do {

            let results = try viewContext.fetch(request)
            // Update the state property
            self.people = results
        } catch {

        }
    }

}
``