Exception when setting an optional binding to nil in SwiftUI after checking - exception

I have a view with a State variable which is an Optional. I render the view by first checking if the optional variable is nil, and, if it is not, force unwrapping it and passing it into a subview using a Binding.
However, if I toggle the optional variable between a value and nil, the app crashes and I get a EXC_BAD_INSTRUCTION in the function BindingOperations.ForceUnwrapping.get(base:). How can I get the expected functionality of the view simply displaying the 'Nil' Text view?
struct ContentView: View {
#State var optional: Int?
var body: some View {
VStack {
if optional == nil {
Text("Nil")
} else {
TestView(optional: Binding($optional)!)
}
Button(action: {
if optional == nil {
optional = 0
} else {
optional = nil
}
}) {
Text("Toggle")
}
}
}
}
struct TestView: View {
#Binding var optional: Int
var body: some View {
VStack {
Text(optional.description)
Button(action: {
optional += 1
}) {
Text("Increment")
}
}
}
}

I found a solution that doesn't involve manually created bindings and/or hard-coded defaults. Here's a usage example:
if let unwrappedBinding = $optional.withUnwrappedValue {
TestView(optional: unwrappedBinding)
} else {
Text("Nil")
}
If you want, you could also provide a default value instead:
TestView(optional: $optional.defaulting(to: someNonOptional)
Here are the extensions:
protocol OptionalType: ExpressibleByNilLiteral {
associatedtype Wrapped
var optional: Wrapped? { get set }
}
extension Optional: OptionalType {
var optional: Wrapped? {
get { return self }
mutating set { self = newValue }
}
}
extension Binding where Value: OptionalType {
/// Returns a binding with unwrapped (non-nil) value using the provided `defaultValue` fallback.
var withUnwrappedValue: Binding<Value.Wrapped>? {
guard let unwrappedValue = wrappedValue.optional else {
return nil
}
return .init(get: { unwrappedValue }, set: { wrappedValue.optional = $0 })
}
/// Returns an optional binding with non-optional `wrappedValue` (`Binding<T?>` -> `Binding<T>?`).
func defaulting(to defaultValue: Value.Wrapped) -> Binding<Value.Wrapped> {
.init(get: { self.wrappedValue.optional ?? defaultValue }, set: { self.wrappedValue.optional = $0 })
}
}

Here is a possible approach to fix this. Tested with Xcode 12 / iOS 14.
The-Variant! - Don't use optional state/binding & force-unwrap ever :)
Variant1: Use binding wrapper (no other changes)
CRTestView(optional: Binding(
get: { self.optional ?? -1 }, set: {self.optional = $0}
))
Variant2: Transfer binding as-is
struct ContentView: View {
#State var optional: Int?
var body: some View {
VStack {
if optional == nil {
Text("Nil")
} else {
CRTestView(optional: $optional)
}
Button(action: {
if optional == nil {
optional = 0
} else {
optional = nil
}
}) {
Text("Toggle")
}
}
}
}
struct CRTestView: View {
#Binding var optional: Int?
var body: some View {
VStack {
Text(optional?.description ?? "-1")
Button(action: {
optional? += 1
}) {
Text("Increment")
}
}
}
}

Related

SwiftUI JSON Decoder Response is 0 for APIResponse

I'm trying to fetch urls from an API based on a search query. If I hardcode the query parameter to some value in the url (i.e. "fitness"), I get a response.
If I set the query parameter to an interpolated value to be inserted at a later date, the app has no images at runtime-- which makes sense.
However, when I enter a search query into my search bar, I cannot fetch the results, either. In fact, my results are 0.
Here's the error:
po jsonResult
▿ APIResponse
- total : 0
- results : 0 elements
Here's my code:
Models
import Foundation
struct APIResponse: Codable {
let total: Int
let results: [Result]
}
struct Result: Codable {
let id: String
let urls: URLS
}
struct URLS: Codable {
let full: String
}
View
import SwiftUI
struct SimpleView: View {
#ObservedObject var simpleViewModel = SimpleViewModel.shared
#State private var searchText = ""
#State private var selected: String? = nil
var filteredResults: [Result] {
if searchText.isEmpty {
return simpleViewModel.results
} else {
return simpleViewModel.results.filter { $0.urls.full.contains(searchText) }
}
}
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 0) {
ForEach(filteredResults, id: \.id) { result in
NavigationLink(destination: SimpleDetailView()) {
VStack {
AsyncImage(url: URL(string: result.urls.full)) { image in
image.resizable()
} placeholder: {
ProgressView()
}
.scaledToFill()
.frame(maxWidth: .infinity)
.onTapGesture {
withAnimation(.spring()) {
if self.selected == result.urls.full {
self.selected = nil
} else {
self.selected = result.urls.full
}
}
hideKeyboard()
}
.scaleEffect(self.selected == result.urls.full ? 3.0 : 1.0)
}
}
}
}
}
.onAppear {
simpleViewModel.fetchPhotos(query: searchText)
}
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
}
}
}
struct SimpleView_Previews: PreviewProvider {
static var previews: some View {
SimpleView()
}
}
ViewModel
import Foundation
class SimpleViewModel: ObservableObject {
static let shared = SimpleViewModel()
private init() {}
#Published var results = [Result]()
func fetchPhotos(query: String) {
let url = "https://api.unsplash.com/search/photos?page=1&query=\(query)&client_id=blahblahblahblahblahblahblahblah"
guard let url = URL(string: url) else { return }
let task = URLSession.shared.dataTask(with: url) { [weak self] data, _, error in
guard let data = data, error == nil else { return }
do {
let jsonResult = try JSONDecoder().decode(APIResponse.self, from: data)
DispatchQueue.main.async {
self?.results = jsonResult.results
}
} catch {
print("Error: \(error)")
}
}
task.resume()
}
}
How can I search for images in my SimpleView based on my search query in my SimpleViewModel?
Setting breakpoints (how I discovered 0 values)
Ternary operators to check for search values or not
Setting my computer on fire
UPDATE
I added this to the code as #workingdog suggested, but with an else statement.
.onSubmit(of: .search) {
if searchText.isEmpty {
simpleViewModel.results = filteredResults
} else if !searchText.isEmpty {
simpleViewModel.fetchPhotos(query: searchText)
}
}
Here's what happens:
Images are fetched, but not displayed in view
Search query on submit renders nothing
Pressing cancel enacts the query
The images are displayed
Images remain and are displayed. Go back to 2.
In your SimpleView, the .onAppear { simpleViewModel.fetchPhotos(query: searchText } is
called only when the view appears, and uses searchText = "". In other words you have an empty query. So remove the .onAppear{...}, it does nothing.
Add something like this, to fetch the photos when the searchText
is submitted.
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.onSubmit(of: .search) {
if !searchText.isEmpty {
simpleViewModel.fetchPhotos(query: searchText)
}
}

SwiftUI: Instance member 'enableFilter' cannot be used on type 'ContentView'; did you mean to use a value of this type instead?

I'm a beginner with SwiftUI and I wanted to create a dynamic list with the birthdays of people. I also wanted to integrate a filter that facilitates to find birthdays. But in the lines:
if enableFilter == true {
return json.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return json
}
I always get these errors:
Instance member 'enableFilter' cannot be used on type 'ContentView';
did you mean to use a value of this type instead?
and
Instance member 'filterDate' cannot be used on type 'ContentView'; did
you mean to use a value of this type instead?
I think I understand why the errors are present but I don't no how to fix it. I tried:
#State static var
but then I cannot change the values with my
filterView
Thank you for your help, here is the full source code:
import SwiftUI
struct person: Codable, Hashable, Identifiable {
var id: Int
var Birthday: Date
var BirthdayString: String
}
func filter(date: Date) -> String {
let DateComponents = Calendar.current.dateComponents([.year, .month, .day], from: date)
let DateComponentsString: String = "\(DateComponents.day)/\(DateComponents.month)/\(DateComponents.year)"
return DateComponentsString
}
struct ContentView: View {
#State var people: [person] = {
guard let data = UserDefaults.standard.data(forKey: "people") else { return [] }
if let json = try? JSONDecoder().decode([person].self, from: data) {
if enableFilter == true {
return json.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return json
}
}
return []
}()
#State var filterDate: Date = Date()
#State var enableFilter: Bool = false
#State var showFilter: Bool = false
#State var newPersonDate: Date = Date()
var body: some View {
NavigationView {
VStack {
HStack {
DatePicker(selection: $newPersonDate, label: {Text("Birthday")}).padding()
Button(action: {didTapAddTask()}, label: {Text("Add")}).padding()
}
List {
ForEach($people) { $person in
Text("\(person.Birthday)")
}
}
}
.navigationTitle(Text("People's birthday"))
}
}
var filterView: some View {
VStack {
DatePicker(selection: $filterDate, label: {Text("Date")}).padding()
Toggle(isOn: $enableFilter, label: {Text("enable filter")}).padding()
}
}
func didTapAddTask() {
let id = people.reduce(0) { max($0, $1.id) } + 1
people.insert(person(id: id, Birthday: newPersonDate, BirthdayString: filter(date: newPersonDate)), at: 0)
newPersonDate = Date()
save()
}
func save() {
guard let data = try? JSONEncoder().encode(people) else { return }
UserDefaults.standard.set(data, forKey: "people")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can use another Computed property which will get the data based on some another property's value. i.e enableFilter.
#State var people: [person] = {
guard let data = UserDefaults.standard.data(forKey: "people") else { return [] }
if let json = try? JSONDecoder().decode([person].self, from: data) {
return json
}
return []
}()
var data : [person] {
if enableFilter {
return people.filter {$0.BirthdayString.contains(filter(date: filterDate))}
} else {
return people
}
}
And use this property to get the relevant data:
var body: some View {
NavigationView {
VStack {
HStack {
DatePicker(selection: $newPersonDate, label: {Text("Birthday")}).padding()
Button(action: {didTapAddTask()}, label: {Text("Add")}).padding()
}
List {
ForEach(data) { person in // <<--- Here `data`
Text("\(person.Birthday)")
}
}
}
.navigationTitle(Text("People's birthday"))
}
}

Swiftui Creating a load more button for Json data items

I have not gotten to 50 reputation so i could not comment on this question SwiftUI Issue displaying specific number of Json Data Items to ask how it was done. The idea is to have the first 10 items in a json array show when the view is loaded and then a load more button to show more items.
This is how my code looks like.
Group {
HStack {
Text("Recommended Events")
.font(.title3)
.foregroundColor(.white)
.fontWeight(.bold)
Spacer()
Button(action: {
}) {
Text("Show all")
.font(.title3)
.foregroundColor(Color.white)
.fontWeight(.bold)
}
}
.padding(15)
ForEach(recommendeds) { recommended in
NavigationLink(destination: RecommendedEventsDetailView(recommended: recommended)) {
RecommendedEventsView(recommended: recommended)
}
}
}
Edited
After https://stackoverflow.com/users/14733292/raja-kishan response i tried it and got this error Failed to produce diagnostic for expression; please file a bug report This is the stage of my code now.
struct RecommendedModel: Identifiable {
var id = UUID()
var number: Int
init(_ number: Int) {
self.number = number
}
}
struct PlacesView: View {
private var arrData: [RecommendedModel] = (0...10).map({RecommendedModel($0)})
#State private var isMore: Bool = false
//E-MARK: - Body
var body: some View {
Group {
HStack {
Text("Recommended Events")
.font(.title3)
.foregroundColor(.white)
.fontWeight(.bold)
Spacer()
Button(action: {
withAnimation {
isMore.toggle()
}
}) {
Text("Show all")
.font(.title3)
.foregroundColor(Color.white)
.fontWeight(.bold)
}
}
.padding(15)
ForEach( (isMore ? arrData : recommendeds(arrData.prefix(5)))) { recommended in
NavigationLink(destination: RecommendedEventsDetailView(recommended: \(recommended.number)) {
RecommendedEventsView(recommended: \(recommended.number))
}
}
}
}
}
Below is the Data model i had before that loads the json data saved as RecommendedModel.swift
struct Recommended: Codable, Identifiable {
let id: String
let image: String
let date: String
let month: String
let like: String
let rating: String
let heading: String
let place: String
let article: String
let more: String
}
You can do this.
You can load the first 10 or 50 by .prefix() from the array.
Demo code
struct DataModel: Identifiable {
var id = UUID()
var number: Int
init(_ number: Int) {
self.number = number
}
}
struct LoadMoreDemo: View {
private var arrData: [DataModel] = (0...100).map({DataModel($0)})
#State private var isMore: Bool = false
var body: some View {
VStack {
ScrollView {
ForEach( (isMore ? arrData : Array(arrData.prefix(15)))) { item in
Text("\(item.number)")
}
}
Button("Load More") {
withAnimation {
isMore.toggle()
}
}
}
}
}

Search through list (JSON) SwiftUI

I am new to SwiftUI. I am currently doing Tutorials that are available on the Apple Developer website.
I was looking at the 'Handling User Input' part and I have a question. In there they take the JSON file and use it to populate the list. From there they create 'Favourite' toggle. My question is, is there a possibility to make JSON list searchable?
import SwiftUI
struct LandmarkList: View {
#EnvironmentObject var userData: UserData
var body: some View {
NavigationView {
List {
Toggle(isOn: $userData.showFavoritesOnly) {
Text("Favorites only")
}
ForEach(landmarkData) { landmark in
if !self.userData.showFavoritesOnly || landmark.isFavorite {
NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
LandmarkRow(landmark: landmark)
}
}
}
}
.navigationBarTitle(Text("Landmarks"))
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
.environmentObject(UserData())
}
}
I found a way of making search field, which would look something like this:
struct SearchBar: UIViewRepresentable {
#Binding var text: String
var placeholder: String
class Coordinator: NSObject, UISearchBarDelegate {
#Binding var text: String
init(text: Binding<String>) {
_text = text
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
text = searchText
}
}
func makeCoordinator() -> SearchBar.Coordinator {
return Coordinator(text: $text)
}
func makeUIView(context: UIViewRepresentableContext<SearchBar>) -> UISearchBar {
let searchBar = UISearchBar(frame: .zero)
searchBar.delegate = context.coordinator
searchBar.placeholder = placeholder
searchBar.searchBarStyle = .minimal
searchBar.autocapitalizationType = .none
return searchBar
}
func updateUIView(_ uiView: UISearchBar, context: UIViewRepresentableContext<SearchBar>) {
uiView.text = text
}
}
and then calling it, but I can't find a way to search through the list. I found a lot of tutorials showing how to search through array, but that isn't very helpful.
I tried few things, this is one of them, but it doesn't work:
var body: some View {
NavigationView {
VStack {
SearchBar(text: $searchText, placeholder: "Search")
List(LandmarkData.filter{searchText == "" ? true : $0.localizedCaseInsensitiveContains(searchText)}, id: \.self){ landmark in
LandmarkRow(landmark: landmark)
}
}.navigationBarTitle("Landmarks")
}
}
}
Can someone explain to me what I'm doing wrong? Thank you
I found an answer to it:
List {
ForEach(LandmarkData.filter {
$0.name.lowercased().contains(self.searchText) || self.searchText.isEmpty
}, id: \.self) { landmark in
LandmarkRow(landmark: landmark)
}
}
The rest of the code stayed the same :) Hope it will help somebody.

Swift 3 - Declaring a simulated JSON as a response from a server

I'm making an application with swift 3.0. But I have a problem, because in the API REST still have not implemented the service, I'm creating a simulated JSON to continue working. But the problem as you will see at the end of all the explanation in the image is that I do not know how to declare a JSON "-.- .... Basically the program will make a call to the server and it will respond with a JSON (now I pass it "the simulated" you will see it in the code). And with that JSON maps it with ObjectMapper to some models (that I pass the code) so that in the end the application has an object.
Error declaring Simulated JSON
These are the three models I have to map the JSON when it will come from the server or in this case, the simulated JSON.
The first is "LegendEntriesModel":
import Foundation
import ObjectMapper
import AlamofireDomain
class LegendEntriesModel: Mappable {
fileprivate var _id_snapshot: String?
fileprivate var _date: String?
fileprivate var _deliverables: [DeliverablesModel]?
init(){}
required init?(map: Map) { }
func mapping(map: Map) {
self.id_snapshot <- map["id_snapshot"]
self.date <- map["date"]
self.deliverables <- map["deliverables"]
}
var id_snapshot: String {
get {
if _id_snapshot == "" {
_id_snapshot = ""
}
return _id_snapshot!
}
set {
_id_snapshot = newValue
}
}
var date: String {
get {
if _date == "" {
_date = ""
}
return _date!
}
set {
_date = newValue
}
}
var deliverables: [DeliverablesModel] {
get {
if _deliverables == nil {
_deliverables = []
}
return _deliverables!
}
set {
_deliverables = newValue
}
}
//MARK: RELEASE MEMORY BETWEEN OBJECT AND API REST (BROKE DEPENDENCIS)
func copy()->LegendEntriesModel {
let legendEntriesModel = LegendEntriesModel()
legendEntriesModel.id_snapshot = self.id_snapshot
legendEntriesModel.date = self.date
legendEntriesModel.deliverables = copyDeliverables()
return legendEntriesModel
}
func copyDeliverables() -> [DeliverablesModel]{
var newArray: [DeliverablesModel] = []
for item in deliverables {
newArray.append(item.copy())
}
return newArray
}
}
The second on is "DeliverablesModel"
import Foundation
import ObjectMapper
import AlamofireDomain
class DeliverablesModel: Mappable {
fileprivate var _id: String?
fileprivate var _type: String?
fileprivate var _url_layer: String?
fileprivate var _options: OptionsDeliverablesModel?
init(){}
required init?(map: Map) { }
func mapping(map: Map) {
self.id <- map["id"]
self.type <- map["type"]
self.url_layer <- map["url_layer"]
self.options <- map["options"]
}
var id: String {
get {
if _id == "" {
_id = ""
}
return _id!
}
set {
_id = newValue
}
}
var type: String {
get {
if _type == "" {
_type = ""
}
return _type!
}
set {
_type = newValue
}
}
var url_layer: String {
get {
if _url_layer == "" {
_url_layer = ""
}
return _url_layer!
}
set {
_url_layer = newValue
}
}
var options: OptionsDeliverablesModel {
get {
if _options == nil {
_options = OptionsDeliverablesModel()
}
return _options!
}
set {
_options = newValue
}
}
//MARK: RELEASE MEMORY BETWEEN OBJECT AND API REST (BROKE DEPENDENCIS)
func copy()->DeliverablesModel {
let deliverablesModel = DeliverablesModel()
deliverablesModel.id = self.id
deliverablesModel.type = self.type
deliverablesModel.url_layer = self.url_layer
deliverablesModel.options = self.options
return deliverablesModel
}
}
And the last one is "OptionsDeliverablesModel":
import Foundation
import ObjectMapper
import AlamofireDomain
class OptionsDeliverablesModel: Mappable {
fileprivate var _type: String?
fileprivate var _max_range: Float?
fileprivate var _min_range: Float?
fileprivate var _title: String?
fileprivate var _initial_max_value: Float?
fileprivate var _initial_min_value: Float?
fileprivate var _id: String?
init(){}
required init?(map: Map) { }
func mapping(map: Map) {
self.type <- map["type"]
self.max_range <- map["max_range"]
self.min_range <- map["min_range"]
self.title <- map["title"]
self.initial_max_value <- map["initial_max_value"]
self.initial_min_value <- map["initial_min_value"]
self.id <- map["id"]
}
var type: String {
get {
if _type == "" {
_type = ""
}
return _type!
}
set {
_type = newValue
}
}
var max_range: Float {
get {
if _max_range == 0 {
_max_range = 0
}
return _max_range!
}
set {
_max_range = newValue
}
}
var min_range: Float {
get {
if _min_range == 0 {
_min_range = 0
}
return _min_range!
}
set {
_min_range = newValue
}
}
var title: String {
get {
if _title == "" {
_title = ""
}
return _title!
}
set {
_title = newValue
}
}
var initial_max_value: Float {
get {
if _initial_max_value == 0 {
_initial_max_value = 0
}
return _initial_max_value!
}
set {
_initial_max_value = newValue
}
}
var initial_min_value: Float {
get {
if _initial_min_value == 0 {
_initial_min_value = 0
}
return _initial_min_value!
}
set {
_initial_min_value = newValue
}
}
var id: String {
get {
if _id == "" {
_id = ""
}
return _id!
}
set {
_id = newValue
}
}
//MARK: RELEASE MEMORY BETWEEN OBJECT AND API REST (BROKE DEPENDENCIS)
func copy()->OptionsDeliverablesModel {
let optionsDeliverablesModel = OptionsDeliverablesModel()
optionsDeliverablesModel.type = self.type
optionsDeliverablesModel.max_range = self.max_range
optionsDeliverablesModel.min_range = self.min_range
optionsDeliverablesModel.title = self.title
optionsDeliverablesModel.initial_max_value = self.initial_max_value
optionsDeliverablesModel.initial_min_value = self.initial_min_value
optionsDeliverablesModel.id = self.id
return optionsDeliverablesModel
}
}
With these three "Models" are what I can map the JSON inside the class DAO, but here is the problem, because I do not know how to pass my JSON that I have simulated.
The code is as follows:
import AlamofireDomain
import Alamofire
import ObjectMapper
class DeliverablesLegendDAO : SimpleDAO {
var deliverables = Dictionary<String, Any>()
deliverables = [{"legendEntries": [{"id_snapshot": "123","date": "2016-10-20","deliveries": [{"id": "12","type": "RGB","url_layer":"topp:states","options": [{"type": "range","max_range": 100,"min_range": 0,"title": "Option RGB","initial_max_value": 100,"initial_min_value": 0,"id": "depth"}]}]}]}]
func snapshots(_ parameters: String,
callbackFuncionOK: #escaping (LegendEntriesModel)->(),
callbackFunctionERROR: #escaping (Int,NSError)->()) {
Alamofire.request(parameters,
method: .post,
encoding: JSONEncoding.default)
.responseJSON { response in
if response.result.isSuccess{
if let status = response.response?.statusCode {
switch(status){
case 200:
let value = response
let legendEntries = Mapper<LegendEntriesModel>().map(JSONObject: value)
callbackFuncionOK(legendEntries!)
default:
break
}
}
}
else {
var statusCode = -1
if let _response = response.response {
statusCode = _response.statusCode
}
var nsError: NSError = NSError(domain: Constants.UNKNOWN_HTTP_ERROR_MSG,
code: Constants.UNKNOWN_HTTP_ERROR_ID,
userInfo: nil)
if let _error = response.result.error {
nsError = _error as NSError
}
callbackFunctionERROR(statusCode,nsError)
}
}
}
}
As you can see in the image, I am declaring my simulated JSON wrong and then map it with "LegendDeliveriesModel" to an object. How can I do it?
Error declaring simulated JSON
If you need anything else, tell me. I repeat, the problem is in the JSON simulated statement that I do not know how to pass it to DAO and that it maps it.
Hi not sure if you will be open to this, but it will be better to try creating a JSON in file and load it in using Bundle like this :
func loadJsonFrom(fileName: String) -> NSDictionary {
let path = Bundle.main.path(forResource: filename, ofType: "json")
let jsonData = try! Data(contentsOf: URL(fileURLWithPath: path!))
let jsonResult: NSDictionary = try! JSONSerialization.jsonObject(with: jsonData, options: .allowFragments) as! NSDictionary
return jsonResult
}
I think your syntax is wrong for declaring your JSON. Pretty sure declaring Dictionaries inline in swift you only use ["key":"value"]
So just remove all of the { and }
Edit: Sorry, didn't realise it was outside of a method. If you want to do that you have to declare it directly like so
var deliverables = ["legendEntries": ["id_snapshot": "123","date": "2016-10-20","deliveries": ["id": "12","type": "RGB","url_layer":"topp:states","options": ["type": "range","max_range": 100,"min_range": 0,"title": "Option RGB","initial_max_value": 100,"initial_min_value": 0,"id": "depth"]]]]
If you're just using it as mock Data I would also consider making it a let constant rather than a variable