I am currently refactoring authentication for an iOS app in SwiftUI and I am having trouble getting the data associated with a new user saved to the backend API. Essentially when the user signs up, they enter their name, email, username, password and phone number and then when they press Continue to proceed to the next screen the info they inputted gets saved.
Here is the JSON string that gets printed whenever a new user tries to sign up:
{"deviceToken":"59746886b275c31066849badc3d6981ec3e9453e5486f68174310fb30102c90c","deviceName":"iPhone","phone":"9173998955","username":"OHegazy12","password":"Password22","appVersion":"ios(17.1.1) iPhone15,3 uSTADIUM 13.5(1)","deviceID":"9CD0FA32-57F4-4052-A038-6CAAC9564318","name":"Omar Hegazy","email":"[email protected]","bundleID":"com.uSTADIUM.uSTADIUM1","platform":"ios","project":"abd87"}
Here are the errors I get:
Error: The data couldn’t be read because it is missing. Swift.DecodingError.keyNotFound(CodingKeys(stringValue: "user", intValue: nil), Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "data", intValue: nil)], debugDescription: "No value associated with key CodingKeys(stringValue: "user", intValue: nil) ("user").", underlyingError: nil))
Here is all the relevant code:
SignUpView()
struct SignUpView: View {
@State private var name = ""
@State private var email = ""
@State private var username = ""
@State private var password = ""
@State private var phone = ""
@State private var isNavigatingToInterests = false
@ObservedObject var signupState: SignupState
@Environment(\.presentationMode) var presentationMode
@Environment(\.dismiss) var dismiss
let progress: Double = 0.33
var isContinueButtonEnabled: Bool {
!name.isEmpty &&
!email.isEmpty && CaseSensitivity.isValidEmail(email) &&
!username.isEmpty && CaseSensitivity.isValidUsername(username) &&
!password.isEmpty && CaseSensitivity.isValidPassword(password) &&
!phone.isEmpty && CaseSensitivity.isValidPhoneNumber(phone)
}
var body: some View {
NavigationView {
VStack {
Image("launch-logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 191, height: 45)
.padding(.top, -64)
.scaleEffect(1.5)
ZStack {
VStack(alignment: .leading, spacing: 20) {
SegmentedProgressBar(progress: progress, foregroundColor: Color(UIColor.uStadium.uPrimary))
.frame(height: 10)
.padding(.horizontal)
.padding(.top, -15)
HStack {
Text("Personal Information")
.font(
Font.custom("Work Sans", size: 18)
.weight(.bold)
)
.foregroundColor(Color(.label))
Spacer()
Text("Step 1")
.font(Font.custom("Work Sans", size: 14))
.fontWeight(.bold)
.foregroundColor(Color(red: 0.1, green: 0.68, blue: 1))
}
.padding(.horizontal, 24)
TextFieldValidity(placeholder: "Enter your name", text: $name, isValid: true)
TextFieldValidity(placeholder: "Enter your email", text: $email, isValid: CaseSensitivity.isValidEmail(email))
.autocapitalization(.none)
TextFieldValidity(placeholder: "Enter your username", text: $username, isValid: CaseSensitivity.isValidUsername(username))
TextFieldValidity(placeholder: "Enter your password", text: $password, isValid: CaseSensitivity.isValidPassword(password))
TextFieldValidity(placeholder: "Enter phone number", text: $phone, isValid: CaseSensitivity.isValidPhoneNumber(phone))
NavigationLink(destination: PickYourInterestsView(signupState: signupState), isActive: $isNavigatingToInterests) {
EmptyView()
}
Button(action: {
if isContinueButtonEnabled {
signUp()
isNavigatingToInterests = true
}
}) {
Text("Continue")
.foregroundColor(.white)
.font(Font.custom("Work Sans", size: 18))
.padding()
.frame(maxWidth: .infinity)
.background(isContinueButtonEnabled ? Color.blue : Color.gray)
.cornerRadius(10)
}
.disabled(!isContinueButtonEnabled)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 60) // Adjust the top padding as needed
.padding(.horizontal)
.padding(.bottom, 40)
.background {
RoundedRectangle(cornerRadius: 16)
.fill(Color(red: 0.15, green: 0.17, blue: 0.22))
.shadow(color: .black.opacity(0.08), radius: 20, x: 0, y: 6)
.overlay(
RoundedRectangle(cornerRadius: 16)
.inset(by: 0.5)
.stroke(Color(red: 0.88, green: 0.88, blue: 1), lineWidth: 1)
)
}
.padding()
}
}
.background(Color(.systemBackground))
.navigationBarItems(leading: Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "arrow.backward.square.fill")
.imageScale(.large)
.font(.system(size: 24))
.foregroundColor(.gray)
.padding(12)
}))
}
.colorScheme(.dark)
}
}
extension SignUpView {
func signUp() {
let accountData = AccountSignUp2(name: name, email: email, username: username, password: password, phone: phone)
AuthService.shared.signup2(data: accountData) { result, error in
if let error = error {
print("Error: \(error.localizedDescription)")
debugPrint(error)
} else if result != nil {
print("SignUp Successful!")
}
}
}
}
AuthService().signup2
func signup2(data: AccountSignUp2, _ completionHandler: @escaping (_ result: LoginOBJ?, _ error: Error?)->()) {
switch state {
case .loggingIn(_): return
case .signingUp(_): return
default: break
}
self.validUsernames = [:]
self.validEmail = [:]
setLoginCredentials(username: data.username, password: data.password)
let jsonData = try! JSONEncoder().encode(data)
if let jsonString = String(data: jsonData, encoding: .utf8) {
print("Request JSON: \(jsonString)")
}
var request = URLRequest(url: URL(string: "\(SERVER_URL)auth/signup")!)
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.httpMethod = "POST"
request.httpBody = jsonData
let task = URLSession.shared.dataTask(with: request){ [unowned self, completionHandler] data, response, error in
if let error = error {
USTracker.tracker.trackEvent(path: .preLogout, data: ["description" : "signup_method", "event_data" : "\(error.localizedDescription)"])
self.state = .loggedOut(error)
completionHandler(nil, error)
return
}
guard let data = data else {
USTracker.tracker.trackEvent(path: .preLogout, data: ["description" : "signup_method", "event_data" : "data empty"])
self.state = .loggedOut(error)
completionHandler(nil, nil)
return
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let object = try decoder.decode(LoginOBJ.self, from: data) as LoginOBJ
if let data = object.data {
self.checkForReferrence(userid: data.user.id)
self.state = .loggedIn(object)
completionHandler(object, nil)
USTracker.tracker.trackEvent(path: .signUp)
} else {
self.state = .loggedOut(nil)
completionHandler(nil, nil)
}
} catch {
Crashlytics.crashlytics().record(error:error)
self.state = .loggedOut(error)
completionHandler(nil, error)
}
}
self.state = .signingUp(task)
task.resume()
}
/// Updated auth/signup request structure
struct AccountSignUp2: Encodable {
let name: String
let email: String
let username: String
let password: String
let phone: String
let project = "abd87"
var feeds: [Int]?
var keywords: [String]?
var referral: AccountReferral?
let platform = "ios"
let deviceToken = UserDefaults.standard.object(forKey: "ustadiumDeviceToken") as? String
let bundleID = "com.uSTADIUM.uSTADIUM1"
let deviceName = UIDevice.current.name
let appVersion = "ios(\(UIDevice.current.systemVersion)) \(UIDevice.current.modelName) uSTADIUM \(version)(\(build))"
let deviceID = UIDevice.current.identifierForVendor?.uuidString ?? UserDefaults.getDeviceID()
}
struct LoginOBJ: Decodable {
struct Missing: Decodable {
var field: String?
var reason: String?
}
struct Data: Decodable {
var user: UserOBJ
var missing: [LoginOBJ.Missing]
var token: String
var firebaseToken: String
mutating func update(firebase token: String){
self.firebaseToken = token
}
mutating func update(token: String){
self.token = token
}
mutating func update(user: UserOBJ){
self.user = user
}
}
var data: LoginOBJ.Data?
mutating func update(firebase token: String){
self.data?.update(firebase: token)
}
mutating func update(token: String){
self.data?.update(token: token)
}
mutating func update(user: UserOBJ){
self.data?.update(user: user)
}
}
Any and all help would be greatly appreciated! I don't know what is missing or not being read from the data.
Edit: Here is UserObj
struct UserOBJ: uCodable, Identifiable, Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
struct Roles: uCodable {
var id: Int
var name: String
var parsedAt: TimeInterval = Date().timeIntervalSince1970
static var objName: String = "role"
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
parsedAt = Date().timeIntervalSince1970
id = try values.decode(Int.self, forKey: .id)
name = try values.decode(String.self, forKey: .name)
}
}
var id: Int
var nickname: String
var username: String
var email: String?
var phone: String?
var bio: String
var city: String
var facebook_id: String?
var twitter_username: String?
var twitter_id: String?
var isVerified: Bool
var isMegaFan: Bool
var numPosts: Int
var media: [MediaOBJ]?
var feeds: [FeedOBJ]?
var isBlocked: Bool?
var isBanned: Bool?
var roles: [UserOBJ.Roles]?
var meta: MetaOBJ?
var currentBadge: Int?
var parsedAt: TimeInterval = Date().timeIntervalSince1970
var deviceID: String?
var wins: Int?
var losses: Int?
var takesVerified: Int
var firstName: String?
var lastName: String?
var dob: String?
var alreadyFollows: Bool?
var roi: Int?
mutating func setVerified() {
self.takesVerified = 1
}
var isAdmin: Bool {
get {
if let roles = self.roles {
return roles.count > 0
}
return false
}
}
var phoneNo: String {
get {
return self.phone!
}
set {
self.phone = newValue
}
}
static var objName: String = "User"
enum CodingKeys: String, CodingKey {
case type = "type"
case id = "id"
case nickname = "nickname"
case username = "username"
case email = "email"
case phone = "phone"
case bio = "bio"
case city = "city"
case facebook_id = "facebook_id"
case twitter_username = "twitter_username"
case twitter_id = "twitteR_id"
case isVerified = "isVerified"
case isMegaFan
case numPosts = "numPosts"
case media = "media"
case feeds = "feeds"
case isBlocked = "isBlocked"
case isBanned = "isBanned"
case roles = "roles"
case meta = "_meta"
case currentBadge = "currentBadge"
case deviceID = "deviceID"
case wins = "wins"
case losses = "losses"
case takesVerified = "takesVerified"
case firstName = "firstName"
case lastName = "lastName"
case dob = "dob"
case alreadyFollows = "alreadyFollows"
case roi = "roi"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
parsedAt = Date().timeIntervalSince1970
id = try values.decode(Int.self, forKey: .id)
nickname = try values.decode(String.self, forKey: .nickname)
username = try values.decode(String.self, forKey: .username)
email = try values.decodeIfPresent(String.self, forKey: .email)
phone = try values.decodeIfPresent(String.self, forKey: .phone)
bio = try values.decode(String.self, forKey: .bio)
city = try values.decode(String.self, forKey: .city)
facebook_id = try values.decodeIfPresent(String.self, forKey: .facebook_id)
twitter_username = try values.decodeIfPresent(String.self, forKey: .twitter_username)
twitter_id = try values.decodeIfPresent(String.self, forKey: .twitter_id)
isVerified = try values.decode(Bool.self, forKey: .isVerified)
isMegaFan = try values.decode(Bool.self, forKey: .isMegaFan)
numPosts = try values.decode(Int.self, forKey: .numPosts)
media = try values.decodeIfPresent([MediaOBJ].self, forKey: .media)
feeds = try values.decodeIfPresent([FeedOBJ].self, forKey: .feeds)
isBlocked = try values.decodeIfPresent(Bool.self, forKey: .isBlocked)
isBanned = try values.decodeIfPresent(Bool.self, forKey: .isBanned)
roles = try values.decodeIfPresent([UserOBJ.Roles].self, forKey: .roles)
meta = try values.decodeIfPresent(MetaOBJ.self, forKey: .meta)
currentBadge = try values.decodeIfPresent(Int.self, forKey: .currentBadge)
deviceID = try values.decodeIfPresent(String.self, forKey: .deviceID)
wins = try values.decodeIfPresent(Int.self, forKey: .wins)
losses = try values.decodeIfPresent(Int.self, forKey: .losses)
takesVerified = try values.decode(Int.self, forKey: .takesVerified)
firstName = try values.decodeIfPresent(String.self, forKey: .firstName)
lastName = try values.decodeIfPresent(String.self, forKey: .lastName)
dob = try values.decodeIfPresent(String.self, forKey: .dob)
alreadyFollows = try values.decodeIfPresent(Bool.self, forKey: .alreadyFollows)
roi = try values.decodeIfPresent(Int.self, forKey: .roi)
}
var needsVerification: Bool {
phone == nil || takesVerified != 1 || (phone?.isEmpty ?? true) && !VerificationManager.alreadyVerified
}
/// The avatar image for this User.
func getProfileImageURL() -> URL? {
guard let media = media else { return nil }
let avatars = media.filter { $0.displayType == .avatar }
if avatars.count > 0 {
return avatars[0].link
}
// TODO: this is not finding the image. Should fix it
return URL(fileURLWithPath: "DefaultAvatar")
}
/// The attributed username for this user with the "@" prepended.
func attributedUsernameWithAt() -> NSAttributedString {
return self.getAttributedName(prepend: "@", fontSize: 14.0)
}
/// The attributed username for this user.
func attributedUserame() -> NSAttributedString {
return self.getAttributedName(prepend: "", fontSize: 14.0)
}
func attributedAt() -> NSAttributedString {
return self.getAttributedName(prepend: "@", fontSize: 14.0)
}
func getUsername() -> String {
"@" + username
}
func str() -> AttributedString {
let string: AttributedString = AttributedString("@" + getUsername())
if self.isVerified {
string.attachment?.image = UIImage(named: "")
}
return string
}
@ViewBuilder
func attributedFullAt(_ size: Int = 15, _ color: Color = .label) -> some View {
UText(getUsername(), size: size, type: .semiBold, color: color)
.lineLimit(1)
// HStack(alignment: .center, spacing: .none) {
//
// if self.isVerified {
// Image(uiImage: .verifiedBadge)
// .resizable()
// .scaledToFit()
// .frame(width: 8, height: 8, alignment: .center)
// } else if self.isMegaFan {
// Image(uiImage: .megaFanBadge)
// .resizable()
// .frame(width: 12, height: 12, alignment: .leading)
// .padding(.leading, -6)
// } else if let currentBadge = currentBadge, currentBadge != 0 {
// switch currentBadge {
// case 1:
// Image(uiImage: .takesCrown)
// .resizable()
// .frame(width: 10, height: 10, alignment: .leading)
// case 2:
// UText("") //("")
// default:
// UText("")
// }
// }
// }
}
func attributedName() -> AttributedString {
var text = AttributedString(attributedAt().string)
let attachment = NSTextAttachment()
if self.isVerified {
// create our NSTextAttachment
text.attachment?.image = .verifiedBadge
} else if self.isMegaFan {
attachment.image = .megaFanBadge
} else if let currentBadge = currentBadge, currentBadge != 0 {
//CURRENT BADGE
// create our NSTextAttachment
switch currentBadge {
case 1:
attachment.image = .takesCrown
case 2:
text = text + AttributedString("")
default:
break
}
}
return text
}
/// Creates the attributed name with the given prepended string, font name, size, and dictionary of other attributes.
///
/// - Parameter prepend: The optional string to prepend to the resulting attributed string.
/// - Parameter fontname: The font's name.
/// - Parameter fontSize: The size of the font.
/// - Parameter otherAtts: The dictionary of NSAttributedString options.
func getAttributedName(prepend: String?, fontSize: CGFloat, otherAtts: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString {
var attrs: [NSAttributedString.Key : Any] = [:]
if fontSize == 12.0 {
attrs[NSAttributedString.Key.font] = UIFont.uStadium.helveticaBold(ofSize: 12)
} else if fontSize == 14.0 {
attrs[NSAttributedString.Key.font] = UIFont.uStadium.helveticaBold(ofSize: 14)
} else if fontSize == 15.0 {
attrs[NSAttributedString.Key.font] = UIFont.uStadium.helveticaBold(ofSize: 15)
}
if let otherAtts = otherAtts {
for (k, v) in otherAtts {
attrs.updateValue(v, forKey: k)
}
}
var string = NSMutableAttributedString(string: " ", attributes: attrs)
if self.isVerified {
string = NSMutableAttributedString(string: "\(prepend ?? "")\(username) ", attributes: attrs)
// create our NSTextAttachment
let verifiedAttachment = NSTextAttachment()
verifiedAttachment.image = .verifiedBadge
var height: CGFloat = 12.0
if fontSize <= 13.0 {
height = 10.0;
}
let yoffset = -0.5*(fontSize)+(height/2)
verifiedAttachment.bounds = CGRect(x: 0.0, y: yoffset, width: height, height: height)
// wrap the attachment in its own attributed string so we can append it
let verifiedString = NSAttributedString(attachment: verifiedAttachment)
string.append(verifiedString)
} else if self.isMegaFan {
string = NSMutableAttributedString(string: "\(prepend ?? "")\(username) ", attributes: attrs)
// create our NSTextAttachment
let megaFanAttachment = NSTextAttachment()
megaFanAttachment.image = .megaFanBadge
var height: CGFloat = 12.0
if fontSize <= 13.0 {
height = 10.0;
}
let yoffset = -0.5*(fontSize)+(height/2)
megaFanAttachment.bounds = CGRect(x: 0.0, y: yoffset, width: height, height: height)
// wrap the attachment in its own attributed string so we can append it
let megaFanString = NSAttributedString(attachment: megaFanAttachment)
string.append(megaFanString)
} else if let currentBadge = currentBadge, currentBadge != 0 {
//CURRENT BADGE
string = NSMutableAttributedString(string: "\(prepend ?? "")\(username) ", attributes: attrs)
// create our NSTextAttachment
let verifiedAttachment = NSTextAttachment()
switch currentBadge {
case 1:
verifiedAttachment.image = .takesCrown
default:
break
}
var height: CGFloat = 12.0
if fontSize <= 13.0 {
height = 10.0;
}
let yoffset = -0.5*(fontSize)+(height/2)
verifiedAttachment.bounds = CGRect(x: 0.0, y: yoffset, width: height, height: height)
// wrap the attachment in its own attributed string so we can append it
let verifiedString = NSAttributedString(attachment: verifiedAttachment)
string.append(verifiedString)
} else {
string = NSMutableAttributedString(string: "\(prepend ?? "")\(username)", attributes: attrs)
}
return string
}
}
extension UserOBJ: Equatable {
static func == (lhs: UserOBJ, rhs: UserOBJ) -> Bool {
return lhs.id == rhs.id
}
}
class VerificationManager {
static var alreadyVerified: Bool = false
}