Sonntag, 19. Juli 2015

Multiline chart, now with less code

I started adding some convenience classes to SwiftCharts to create popular charts quickly.

This is how now a multiline chart is created:

let chart = LineChart(
    frame: CGRectMake(0, 70, 300, 500),
    chartConfig: ChartConfig(
        xAxisConfig: ChartAxisConfig(from: 2, to: 14, by: 2),
        yAxisConfig: ChartAxisConfig(from: 0, to: 14, by: 2)
    ),
    xTitle: "X axis",
    yTitle: "Y axis",
    lines: [
        (chartPoints: [(2.0, 10.6), (4.2, 5.1), (7.3, 3.0), (8.1, 5.5), (14.0, 8.0)], color: UIColor.redColor()),
        (chartPoints: [(2.0, 2.6), (4.2, 4.1), (7.3, 1.0), (8.1, 11.5), (14.0, 3.0)], color: UIColor.blueColor())
    ]
)
self.view.addSubview(chart.view)

Of course the "low level" api and examples continue to be available, and should be used when the convenience charts don't provide the required level of customization.

More will follow!

Dienstag, 14. Juli 2015

Some utility functions to work with Realm in iOS

I started using Realm today. So far a very good first impression! My initial ~1000 lines of core data shrank to 350 lines, which are also generally easier to understand. It also took me only about 3 hours to port the core data code which I wrote in about 3 days. I hope it continues like this.

Since I was repeating a lot of code, I wrote these helper functions for simple operations, so I can now load and save using mostly 1-liners. I use GCD to execute the read and writes in the background, so the db access doesn't block the UI. I also use a model layer, which is entirely decoupled from the database (*).

Here are the functions:

Update for Swift 2:
func saveObj<T: Object>(obj: T, update: Bool = false, handler: Bool -> ()) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        do {
            let realm = try Realm()
            realm.write {
                realm.add(obj, update: update)
            }
            dispatch_async(dispatch_get_main_queue(), {
                handler(true)
            })
        } catch _ {
            print("Error: creating Realm")
            dispatch_async(dispatch_get_main_queue(), {
                handler(false)
            })
        }
    })
}

//usage:
saveObj(myRealmObj, update: true, handler: myHandler)


func saveObjs<T: Object>(objs: [T], update: Bool = false, handler: Bool -> ()) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        do {
            let realm = try Realm()
            realm.write {
                for obj in objs {
                    realm.add(obj, update: update)
                }
            }
            dispatch_async(dispatch_get_main_queue(), {
                handler(true)
            })
        } catch _ {
            print("Error: creating Realm")
            dispatch_async(dispatch_get_main_queue(), {
                handler(false)
            })
        }
    })
}

//usage:
saveObjs(myRealmObjs, update: true, handler: myHandler)


// mapper is a function that maps a database object of type T to a model object of type U
// filterMaybe filter string as in Realm's docs e.g. "name = 'foo'"
// handler function which gets the model object U, which was generated by mapper, as parameter
func load<T: Object, U>(mapper: T -> U, filter filterMaybe: String? = nil, handler: [U] -> ()) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        do {
            let realm = try Realm()
            
            var results = realm.objects(T)
            if let filter = filterMaybe {
                results = results.filter(filter)
            }
            
            let objs: [T] = results.toArray()
            let models = objs.map{mapper($0)}
            
            dispatch_async(dispatch_get_main_queue(), {
                handler(models)
            })
            
        } catch _ {
            print("Error: creating Realm")
            dispatch_async(dispatch_get_main_queue(), {
                handler([])
            })
        }
    })
}

//usage:
let mapper = {realmObj in
    return MyModelObj(name: realmObj.name, color: realmObj.color)
}
load(mapper) {myModelObjs in
    // do something with models
}


// pred string as in Realm's docs e.g. "name = 'foo'"
func remove<T: Object>(pred: String, handler: Bool -> (), objType: T.Type) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        do {
            let realm = try Realm()
            let results: Results<T> = realm.objects(T).filter(pred)
            realm.write {
                realm.delete(results)
            }
            dispatch_async(dispatch_get_main_queue(), {
                handler(true)
            })
        } catch _ {
            print("Error: creating Realm")
            dispatch_async(dispatch_get_main_queue(), {
                handler(true)
            })
        }
    })
}


extension Results {
    //loads the results into an array    
    func toArray() -> [T] {
        return self.map{$0}
    }
}

extension RealmSwift.List {    
    func toArray() -> [T] {
        return self.map{$0}
    }
}




Swift 1.2:
func saveObj<T: Object>(obj: T, update: Bool = false, handler: Bool -> ()) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        let realm = Realm()
        realm.write {
            realm.add(obj, update: update)
        }
        dispatch_async(dispatch_get_main_queue(), {
            handler(true) // I don't know exactly yet what to return here, since Realm's operations don't return anything. For now indicates method executed successfully.
        })
    })
}

//usage:
saveObj(myRealmObj, update: true, handler: myHandler)


func saveObjs<T: Object>(objs: [T], update: Bool = false, handler: Bool -> ()) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        let realm = Realm()
        realm.write {
            for obj in objs {
                realm.add(obj, update: update)
            }
        }
        dispatch_async(dispatch_get_main_queue(), {
            handler(true)
        })
    })
}

//usage:
saveObjs(myRealmObjs, update: true, handler: myHandler)
    

// mapper is a function that maps a database object of type T to a model object of type U
// filterMaybe filter string as in Realm's docs e.g. "name = 'foo'"
// handler function which gets the model object U, which was generated by mapper, as parameter
func load<T: Object, U>(mapper: T -> U, filter filterMaybe: String? = nil, handler: [U] -> ()) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        var results = Realm().objects(T)
        if let filter = filterMaybe {
            results = results.filter(filter)
        }
            
        let objs: [T] = Realm().objects(T).toArray()
        let models = objs.map{mapper($0)}
            
        dispatch_async(dispatch_get_main_queue(), {
            handler(models)
        })
    })
}

//usage:
let mapper = {realmObj in
    return MyModelObj(name: realmObj.name, color: realmObj.color)
}
load(mapper) {myModelObjs in
    // do something with models
}    
    
 
// pred string as in Realm's docs e.g. "name = 'foo'"
func remove<T: Object>(pred: String, handler: Bool -> (), objType: T.Type) {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
        let realm = Realm()
        let results: Results<T> = realm.objects(T).filter(pred)
        realm.write {
            realm.delete(results)
        }

        dispatch_async(dispatch_get_main_queue(), {
            handler(true)
        })
    })
}

//usage:
remove("name = 'foo'", handler: myHandler, objType: MyRealmObject.self)


extension Results {
    //loads the results into an array    
    func toArray() -> [T] {
        return map(self){$0}
    }
}



I hope this is useful to somebody, and if there's something to improve please add a comment!

(*) This has pros and cons, (roughly), pros: decoupling from db, cleaner models, fixes issue with Realm of not being able to read in the background and use the result objects in the main thread. Cons: performance, since the query result has to be loaded into memory and mapped, but for regular, limited/segmented datasets and considering that loading and parsing is done in the background this is not an issue.

Montag, 13. Juli 2015