Integrating LAMP Web Services Project with SwiftUI

Hi All,

Chris and myself have been working together on a “Class” that intergrates projects requiring integration with Web Services, like access to a MySQL database via PHP.

I would like to share the class with the community, and hope that it serves you as well as it’s serving me.

Example function making use of Continuation in a async function to allow the function to return data, which otherwise would return nil before the async operation completes.

 func fetchData<T:Codable>(url: String) async throws -> T {
        
        print("Function is running")

        return try await withCheckedThrowingContinuation { continuation in
            URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in

                if let error = error {
                    print("Request error: ", error.localizedDescription)
                    return
                }

                guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                    if let response = response {
                        print(response)
                    }
                    return
                }
                guard let data = data else { return }

                let decoder = JSONDecoder()
                do {
                    let decodedData = try decoder.decode(T.self, from: data)
                    continuation.resume(with: .success(decodedData))
                    print(decodedData)
                } catch {
                    print(error.localizedDescription)
                }
            }
            .resume()
        }
    }

I have modified my own “Project” from Chris Parkers direection to allow me to populate views from downloaded JSON data, on a View by basis, since I probably wouldn’t want to download all my View(s) content in one go in the class init, and as such will run my View by View content for that particular view using the .onAppear modifier.

I would probably want to do this on a per VStack basis, or even a HStack basis, but since I only have one List on this View, have decided to perform this content download in the View’s ZStack.

I couldn’t get it to run on a List().onAppear modifier for some reason ?

I will have either a single webservices.php running, which would expect a POST string of a SELECT statement, in which case it will always return custom JSON based on table keys of my SELECT statement, or will have a webservices.php for each View, or for each content.

It allows flexibility to be able to download content in the .onAppear modifier at the moment since it’s just an example, and examples are designed to be played with.

I suspect that I won’t make all my Arrays part of the WebServices class either, so will create instances of them in a View by View basis, and break them out, and set them up as State vars.

I am going to copy and paste my project file by file here to me enable me to create new Web Services style Projects from it in the future, and obviously I wish to share this with the Community so you can do the same.

I can’t always promise that my examples will be available on my web server, so will at least give you the PHP file I used for this example.

download_myfamily_users.php

<?php

$servername = "localhost";
$username = "root";
$password = "**********";
$db = "myfamily";

// Create connection
$conn = new mysqli($servername, $username, $password, $db);
// Check connection
if ($conn->connect_error) {
  die("Connection failed: " . $conn->connect_error);
}

if ($sth = mysqli_query($conn, "SELECT * FROM tUsers WHERE 1 ORDER BY user_id ASC")) {

  $rows = array();

  while($r = mysqli_fetch_assoc($sth)) {
      $rows[] = $r;
  }

  echo json_encode($rows,JSON_NUMERIC_CHECK);
}
?>

To allow it to run with your own database, you will need to update the DataObject (below), and update the database, table, and the SELECT statement in the PHP above.

I think it’s a very worth while “handy” class that we have created between myself and Chris Parker.

I will leave some copy and paste code here.

WebServices

//
//  WebServices.swift
//
//  Created by Chris Parker on 24/2/22
//  Copyright © 2022 Chris Parker. All rights reserved.
//

import SwiftUI

class WebServices: ObservableObject  {

    // Call this in Views to set up a new instance of this class.
    // @ObservableObject var dataViewModel: webServices = webServices() -> SUBVIEWS
    // @StateObject var dataViewModel: webServices = webServices() -> MAIN VIEW

    // This isn't currently updating any views, so make it a variable that can be written to.
    // It is being referenced in my view, so I can't make it private.

    // Publish a copy of the arrDataRecv struct so that it can update my View
    @Published var myTestArrayRX: [ArrDataRecv] = []

    init() {
        
       // runFetchData()
    }

    // Functions
    func encodeJSON<T: Codable>(object: T) -> Data? {
        // Try to encode JSON data to send to my Webserver
        // Will do, do, big doggy do do, guard etc
        let encoder = JSONEncoder()
        let resultJSONData = try! encoder.encode(object)
        //print(String(data: resultJSONData, encoding: .utf8)!)
        return resultJSONData
    }

    func decodeJSON<T:Codable>(data : Data?) -> T? {
        guard let data = data else { return nil }
        do {
            let object = try JSONDecoder().decode(T.self, from: data)
            return object
        } catch  {
            print("Unable to decode", error.localizedDescription)
        }
        return nil
    }

    func uploadData<T:Codable, A:Codable>(json: A, url: String) async -> T? {
        // This URL sends back what it recieves.
        // Great for testing JSON
        let url = URL(string: url.self)!

        var request = URLRequest(url: url)
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpMethod = "POST"

        // Try to encode JSON data to send to my Webserver
        let resultJSONData = encodeJSON(object: json)!

        do {
            let(dataFromServer, _) = try await URLSession.shared.upload(for: request, from: resultJSONData)

            if let result: T = decodeJSON(data: dataFromServer) {
                return result
            }
        } catch {
            print("Upload Failed")
            return nil
        }
        // Should i return nil ?
        return nil
    }

    func runFetchData() {
        let urlString = "https://myswift.co.uk/familyapp/download_myfamily_users.php"
        Task {
            if let result: [ArrDataRecv] = try await fetchData(url: urlString) {
                DispatchQueue.main.async {
                    self.myTestArrayRX = result

                }
            }
        }
    }

    func fetchData<T:Codable>(url: String) async throws -> T {
        
        print("Function is running")

        return try await withCheckedThrowingContinuation { continuation in
            URLSession.shared.dataTask(with: URL(string: url)!) { (data, response, error) in

                if let error = error {
                    print("Request error: ", error.localizedDescription)
                    return
                }

                guard (response as? HTTPURLResponse)?.statusCode == 200 else {
                    if let response = response {
                        print(response)
                    }
                    return
                }
                guard let data = data else { return }

                let decoder = JSONDecoder()
                do {
                    let decodedData = try decoder.decode(T.self, from: data)
                    continuation.resume(with: .success(decodedData))
                    print(decodedData)
                } catch {
                    print(error.localizedDescription)
                }
            }
            .resume()
        }
    }

    enum NetworkError: Error {
        case badUrl
        case invalidRequest
    }
}

DataObject

//
//  DataObject.swift
//
//  Created by Chris Parker on 24/2/22
//  Copyright © 2022 Chris Parker. All rights reserved.
//

import Foundation


// Second Array for storing data receieved from my web service.
struct ArrDataRecv: Codable, Identifiable {
    var id: Int
    var firstName: String
    var surName: String
    var avatar: String

    enum CodingKeys: String, CodingKey {
        case id = "user_id"
        case firstName = "user_firstname"
        case surName = "user_surname"
        case avatar = "user_avatarURL"
    }
}

ContentView

//
//  ContentView.swift
//  alantrundle3
//
//  Created by Chris Parker on 23/2/22
//  Copyright © 2022 Chris Parker. All rights reserved.
//

import SwiftUI

// MAIN VIEW - HOME
struct ContentView: View {
    // @EnvironmentObject var dataViewModel: WebServices
    @StateObject var dataViewModel: WebServices = WebServices()
    
    init()
    {
        let navBarAppearance = UINavigationBarAppearance()
        navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.white]
        navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.white]
        navBarAppearance.backgroundColor = .blue
        
        UINavigationBar.appearance().standardAppearance = navBarAppearance
        UINavigationBar.appearance().compactAppearance = navBarAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
        UINavigationBar.appearance().tintColor = UIColor.systemBackground
    }
    
    var body: some View {
        
        NavigationView {
            ZStack {
                VStack(alignment: .leading) {
                    if dataViewModel.myTestArrayRX.isEmpty {
                        ProgressView()
                    } else {
                        Text("Family Members")
                            .font(.title)
                            .padding()
                        List {
                            ForEach(dataViewModel.myTestArrayRX) { item in
                                
                                NavigationLink(destination: AvatarScreen(item: item)) {
                                    
                                    HStack() {
                                        Image(systemName: "person.fill")
                                            .foregroundColor(.blue)
                                        Text(item.firstName + " " + item.surName)
                                            .foregroundColor(.blue)
                                        Spacer()
                                    }
                                }
                            }
                        }
                        .listStyle(PlainListStyle())
                    }
                }
                .navigationTitle("WS Example")
                .navigationBarTitleDisplayMode(.inline)
                
            }.onAppear {
                Task {
                    let urlString = "https://myswift.co.uk/familyapp/download_myfamily_users.php"
                    if let result: [ArrDataRecv] = try await dataViewModel.fetchData(url: urlString) {
                        DispatchQueue.main.async {
                            dataViewModel.myTestArrayRX = result
                            
                        }
                    }
                    else { print("Error") }
                }
                
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(WebServices())
    }
}

AvatarScreen

//
//  AvatarScreen.swift
//
//  Created by Chris Parker on 24/2/22
//  Copyright © 2022 Chris Parker. All rights reserved.
//

import SwiftUI

// AVATAR SCREEN - AVATAR VIEW
struct AvatarScreen: View {
   // @EnvironmentObject var dataViewModel: WebServices
    @Environment(\.presentationMode) var presentation
    let item: ArrDataRecv

    var body: some View {

        ZStack {
            Color.mint
                .ignoresSafeArea()

            VStack {
                HStack {
                    Text(item.firstName + "'s Avatar")
                        .font(.largeTitle)
                        .foregroundColor(.white)
                        .multilineTextAlignment(.center)
                        .padding([.top, .leading], 20.0)

                    Spacer()
                }



                Spacer()

            }

            VStack {

                AsyncImage(url: URL(string: item.avatar)) { image in
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width:200, height:200)
                } placeholder: {
                    ProgressView()
                }

            }
        }
        .navigationTitle("My Avatar")
        .navigationBarBackButtonHidden(true)
        .navigationBarItems(leading: Button(action : {
            self.presentation.wrappedValue.dismiss()
        }){
            ZStack {
                Circle()
                    .fill(Color.black)
                    .frame(width:25, height: 25)

                Image(systemName: "arrow.left")
                    .foregroundColor(Color.white)
            }
        })
    }
}

struct AvatarScreen_Previews: PreviewProvider {
    static var previews: some View {
        AvatarScreen(item: ArrDataRecv.mockData)
            .environmentObject(WebServices())
    }
}

A big shout out to Chris Parker for his time, his patience and his expert knowledge.
Chris - I couldn’t have completed this without you!

Kind Regards

Alan

2 Likes