Caching and Restoring TabBarItem Order - swift

Is there a simple way to do this for a UITabBarController after the user has modified the original ordering? I got the following code to work but it feels a bit clunky.
class TabBarViewController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.restoreItemOrder()
}
override func tabBar(_ tabBar: UITabBar, didEndCustomizing items: [UITabBarItem], changed: Bool) {
if changed {
self.cacheItemOrder(items: items)
}
}
}
The extensions to cache/restore the order after editing.
extension UITabBarController {
private static let cacheKey = "tabBarIndex"
public func cacheItemOrder(items: [UITabBarItem]) {
var index = [String: Int]()
var i = 0
for item in items {
if let title = item.title {
index[title] = i
}
i += 1
}
UserDefaults.standard.set(index, forKey: UITabBarController.cacheKey)
}
public func restoreItemOrder() {
if let dict = UserDefaults.standard.dictionary(forKey: UITabBarController.cacheKey) as? [String: Int] {
if let vcs = self.viewControllers {
self.viewControllers = vcs.sorted() {
var idx0 = 99
var idx1 = 99
if let t0 = $0.tabBarItem.title, let idx = dict[t0] {
idx0 = idx
}
if let t1 = $1.tabBarItem.title, let idx = dict[t1] {
idx1 = idx
}
return idx0 < idx1
}
}
}
}
}
Thanks in advance.

You can express everything more succinctly with functional idioms:
extension UITabBarController {
private static let cacheKey = "tabBarIndex"
public func cacheItemOrder(items: [UITabBarItem]) {
UserDefaults.standard.set(
[String: Int](uniqueKeysWithValues: items.enumerated().map { ($1.title ?? "", $0)}),
forKey: UITabBarController.cacheKey
)
}
public func restoreItemOrder() {
guard let order = UserDefaults.standard.dictionary(forKey: UITabBarController.cacheKey) as? [String: Int] else {
return
}
viewControllers?.sort { left, right in
guard let leftIndex = left.title.flatMap({order[$0]}),
let rightIndex = right.title.flatMap({order[$0]}) else {
return false
}
return leftIndex < rightIndex
}
}
}

Related

use a switch to change the value of boolean saved core data value

In my swift code below right now the code saves 2 boolean values to a core data boolean value. The two values are true false. I would like the user to turn on the switch and have the 2nd value be true as well. So it would be true true. I am trying to do that in func alternate() but I dont know how to exactly effect a specific value. Looking for any kind of help.
import UIKit;import CoreData
class ViewController: UIViewController {
var lbl = UILabel()
var sw = UISwitch()
var checkmarkButton = UIButton()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
[ lbl,sw,checkmarkButton].forEach {
view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
$0.layer.borderWidth = 1
}
helpBool.shareInstance.saveBoolean(true)
helpBool.shareInstance.saveBoolean(false)
getBool(imageNo: 1)
}
func getBool(imageNo:Int) {
// first check the array bounds
let info = helpBool.shareInstance.fetchBool()
if info.count > imageNo {
// if info[imageNo].bool {
if info[imageNo].bool == true {
checkmarkButton.setImage(UIImage(named:"unnamed"), for: .normal);
}
if info[imageNo].bool == false {
checkmarkButton.setImage(nil, for: .normal);
}
// }
}
}
#objc func alternate(){
//fetch alternate
getBool(imageNo: 1)
if sw.isOn = true {
helpBool.shareInstance.saveBoolean()
}
else {
helpBool.shareInstance.saveBoolean()
}
}
}
class helpBool: UIViewController{
static let shareInstance = helpBool()
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
func saveBoolean(_ boolean: Bool) {
let imageInstance = OnOff(context: context)
imageInstance.bool = boolean
do {
try context.save()
print("text is saved")
} catch {
print(error.localizedDescription)
}
}
func fetchBool() -> [OnOff] {
var fetchingImage = [OnOff]()
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "OnOff")
do {
fetchingImage = try context.fetch(fetchRequest) as! [OnOff]
} catch {
print("Error while fetching the image")
}
return fetchingImage
}
}

I can print data but can't assign it to a label in Swift

I sent my data from my API call to my InfoController viewDidLoad. There, I was able to safely store it in a skillName constant, and also printed it, receiving all the information by console.
The problem comes when I try to assign this variable to my skillLabel.
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
guard var skillName = names as? String else { return }
self.pokemon?.skillName = skillName
self.allNames = skillName
print(self.allNames)
}
}
There, when I print allNames, the console shows all the data I need. This is how the data looks like: Data Example
And the computed property where I wanna use this data looks is:
var pokemon: Pokemon? {
didSet {
guard let id = pokemon?.id else { return }
guard let data = pokemon?.image else { return }
navigationItem.title = pokemon?.name?.capitalized
infoLabel.text = pokemon?.description
infoView.pokemon = pokemon
if id == pokemon?.id {
imageView.image = UIImage(data: data)
infoView.configureLabel(label: infoView.skillLabel, title: "Skills", details: "\(allNames)")
}
}
}
PD: allNames is a String variable I have at InfoController class-level.
This is how my app looks when run:
PokeApp
My goal is to get that details param to show the skillName data, but it returns nil, idk why. Any advice?
EDIT1: My func that fetches the Pokemon data from my service class is this one:
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
DispatchQueue.main.async {
self.pokemon? = poke
guard let skills = poke.abilities else { return }
for skill in skills {
guard let ability = skill.ability else { return }
guard var names = ability.name!.capitalized as? String else { return }
self.pokemon?.skillName = names
handler(names)
}
}
}
}
EDIT2: InfoView class looks like:
class InfoView: UIView {
// MARK: - Properties
var delegate: InfoViewDelegate?
// This whole block assigns the attributes that will be shown at the InfoView pop-up
// It makes the positioning of every element possible
var pokemon: Pokemon? {
didSet {
guard let pokemon = self.pokemon else { return }
guard let type = pokemon.type else { return }
guard let defense = pokemon.defense else { return }
guard let attack = pokemon.attack else { return }
guard let id = pokemon.id else { return }
guard let height = pokemon.height else { return }
guard let weight = pokemon.weight else { return }
guard let data = pokemon.image else { return }
if id == pokemon.id {
imageView.image = UIImage(data: data)
}
nameLabel.text = pokemon.name?.capitalized
configureLabel(label: typeLabel, title: "Type", details: type)
configureLabel(label: pokedexIdLabel, title: "Pokedex Id", details: "\(id)")
configureLabel(label: heightLabel, title: "Height", details: "\(height)")
configureLabel(label: defenseLabel, title: "Defense", details: "\(defense)")
configureLabel(label: weightLabel, title: "Weight", details: "\(weight)")
configureLabel(label: attackLabel, title: "Base Attack", details: "\(attack)")
}
}
let skillLabel: UILabel = {
let label = UILabel()
return label
}()
let imageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
return iv
}()
. . .
}
infoView.configureLabel is this:
func configureLabel(label: UILabel, title: String, details: String) {
let attributedText = NSMutableAttributedString(attributedString: NSAttributedString(string: "\(title): ", attributes: [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: Colors.softRed!]))
attributedText.append(NSAttributedString(string: "\(details)", attributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.gray]))
label.attributedText = attributedText
}
EDIT 3: Structures design
struct Pokemon: Codable {
var results: [Species]?
var abilities: [Ability]?
var id, attack, defense: Int?
var name, type: String?
...
}
struct Ability: Codable {
let ability: Species?
}
struct Species: Codable {
let name: String?
let url: String?
}
Jump to the Edit2 paragraph for the final answer!
Initial Answer:
I looks like you UI does not get updated after the controller fetches all the data.
Since all of you UI configuration code is inside the var pokemon / didSet, it's a good idea to extract it to a separate method.
private func updateView(with pokemon: Pokemon?, details: String?) {
guard let id = pokemon?.id, let data = pokemon?.image else { return }
navigationItem.title = pokemon?.name?.capitalized
infoLabel.text = pokemon?.description
infoView.pokemon = pokemon
if id == pokemon?.id {
imageView.image = UIImage(data: data)
infoView.configureLabel(label: infoView.skillLabel, title: "Skills", details: details ?? "")
}
}
and now you can easily call in the the didSet
var pokemon: Pokemon? {
didSet { updateView(with: pokemon, details: allNames) }
}
and fetchPokemons completion aswell
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
guard var skillName = names as? String else { return }
self.pokemon?.skillName = skillName
self.allNames = skillName
print(self.allNames)
DispatchQueue.main.async {
self.updateView(with: self.pokemon, details: self.allNames)
}
}
}
It's super important to do any UI setup on the main queue.
Edit:
The fetch function may be causing the problems! you are calling handler multiple times:
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
DispatchQueue.main.async {
self.pokemon? = poke
guard let skills = poke.abilities else { return }
let names = skills.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")
handler(names)
}
}
}
Edit2:
After looking at your codebase there are a couple of things you need to change:
1. fetchPokemons implementation
the handler of controller.service.fetchPokes gets called for every pokemon so we need to check if the fetched one is the current (self.pokemon) and then call the handler with properly formated skills.
func fetchPokemons(handler: #escaping (String) -> Void) {
controller.service.fetchPokes { (poke) in
guard poke.id == self.pokemon?.id else { return }
self.pokemon? = poke
let names = poke.abilities?.compactMap { $0.ability?.name?.capitalized }.joined(separator: ", ")
handler(names ?? "-")
}
}
2. update viewDidLoad()
now simply pass the names value to the label.
override func viewDidLoad() {
super.viewDidLoad()
configureViewComponents()
fetchPokemons { (names) in
self.pokemon?.skillName = names
self.infoView.configureLabel(label: self.infoView.skillLabel, title: "Skills", details: names)
}
}
3. Refactor var pokemon: Pokemon? didSet observer
var pokemon: Pokemon? {
didSet {
guard let pokemon = pokemon, let data = pokemon.image else { return }
navigationItem.title = pokemon.name?.capitalized
infoLabel.text = pokemon.description!
infoView.pokemon = pokemon
imageView.image = UIImage(data: data)
}
}

CANT RESOLVE: unsafeAddressOf is abandoned in Swift 3

I just realized that my old app is not working anymore because unsafeAddressOf is abandoned in Swift 3. I have been searching in Apple documentations and online tutorials but still cant figure out how to change my code to be compliant with Swift 3. Here is my code:
import UIKit
import ImageIO
extension UIImage {
public class func gifWithData(data: NSData) -> UIImage? {
guard let source = CGImageSourceCreateWithData(data, nil) else {
print("SwiftGif: Source for the image does not exist")
return nil
}
return UIImage.animatedImageWithSource(source: source)
}
public class func gifWithName(name: String) -> UIImage? {
guard let bundleURL = Bundle.main.url(forResource: name, withExtension: "gif") else {
print("SwiftGif: This image named \"\(name)\" does not exist")
return nil
}
guard let imageData = NSData(contentsOfURL: bundleURL) else {
print("SwiftGif: Cannot turn image named \"\(name)\" into NSData")
return nil
}
return gifWithData(imageData)
}
class func delayForImageAtIndex(index: Int, source: CGImageSource!) -> Double {
var delay = 0.1
// Get dictionaries
let cfProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil)
let gifProperties: CFDictionary = unsafeBitCast(CFDictionaryGetValue(cfProperties, unsafeAddressOf(kCGImagePropertyGIFDictionary)), to: CFDictionary.self)
// Get delay time
var delayObject: AnyObject = unsafeBitCast(
CFDictionaryGetValue(gifProperties,
unsafeAddressOf(kCGImagePropertyGIFUnclampedDelayTime)),
to: AnyObject.self)
if delayObject.doubleValue == 0 {
delayObject = unsafeBitCast(CFDictionaryGetValue(gifProperties,
unsafeAddressOf(kCGImagePropertyGIFDelayTime)), to: AnyObject.self)
}
delay = delayObject as! Double
if delay < 0.1 {
delay = 0.1 // Make sure they're not too fast
}
return delay
}
class func gcdForPair( a: Int?, var _ b: Int?) -> Int {
// Check if one of them is nil
var a = a
if b == nil || a == nil {
if b != nil {
return b!
} else if a != nil {
return a!
} else {
return 0
}
}
// Swap for modulo
if a < b {
let c = a
a = b
b = c
}
// Get greatest common divisor
var rest: Int
while true {
rest = a! % b!
if rest == 0 {
return b! // Found it
} else {
a = b
b = rest
}
}
}
class func gcdForArray(array: Array<Int>) -> Int {
if array.isEmpty {
return 1
}
var gcd = array[0]
for val in array {
gcd = UIImage.gcdForPair(val, gcd)
}
return gcd
}
class func animatedImageWithSource(source: CGImageSource) -> UIImage? {
let count = CGImageSourceGetCount(source)
var images = [CGImage]()
var delays = [Int]()
// Fill arrays
for i in 0..<count {
// Add image
if let image = CGImageSourceCreateImageAtIndex(source, i, nil) {
images.append(image)
}
// At it's delay in cs
let delaySeconds = UIImage.delayForImageAtIndex(index: Int(i),
source: source)
delays.append(Int(delaySeconds * 1000.0)) // Seconds to ms
}
// Calculate full duration
let duration: Int = {
var sum = 0
for val: Int in delays {
sum += val
}
return sum
}()
// Get frames
let gcd = gcdForArray(array: delays)
var frames = [UIImage]()
var frame: UIImage
var frameCount: Int
for i in 0..<count {
frame = UIImage(CGImage: images[Int(i)])
frameCount = Int(delays[Int(i)] / gcd)
for _ in 0..<frameCount {
frames.append(frame)
}
}
// Heyhey
let animation = UIImage.animatedImage(with: frames,
duration: Double(duration) / 1000.0)
return animation
}
}
Does anyone have an idea how I can fix this code?

KIF + Swift reset/restart app

I'm developing automated tests using KIF and Swift
I wrote some tests but they don't depend on each other
How to reset app and execute this in beforeEach() method?
class PassengerCountTests: KIFTestCase {
enum Element: String {
case passengerCount = "passengerCount_label"
case plusButton = "plus_button"
case minusButton = "minus_button"
}
func getElement(element: Element) -> UIView {
return tester().waitForView(withAccessibilityLabel: element.rawValue)
}
func tap(element: Element) {
tester().tapView(withAccessibilityLabel: element.rawValue)
}
func testMinPassengerNumber() {
let passengerCount = getElement(element: Element.passengerCount)
let minusButton = getElement(element: Element.minusButton)
XCTAssertEqual(passengerCount.accessibilityValue, "1")
XCTAssertEqual(minusButton.isProbablyTappable, false)
}
func testMaxPassengerNumber() {
for _ in 1...4 {
tap(element: Element.plusButton)
}
let passengerCount = getElement(element: Element.passengerCount)
let plusButton = getElement(element: Element.plusButton)
XCTAssertEqual(passengerCount.accessibilityValue, "5")
XCTAssertEqual(plusButton.isProbablyTappable, false)
}
}

How can I sort array with two kind of objects in Swift?

This is my method when I sort elements by position property. Both DBSet and DBItem have that property.
#objc(DBCategory)
class DBCategory: NSManagedObject {
#NSManaged var identifier: String
#NSManaged var items: Set<DBItem>
#NSManaged var sets: Set<DBSet>
}
And this is how I use it
private var elements = [AnyObject]()
private func prepareElements() {
elements.removeAll(keepCapacity: false)
if let items = currentCategory?.items {
for item in items {
elements.append(item)
}
}
if let sets = currentCategory?.sets {
for set in sets {
elements.append(set)
}
}
elements.sort {
var previousPosition = 0
var currentPosition = 0
if let set = $0 as? DBSet {
previousPosition = Int(set.position)
}
if let item = $0 as? DBItem {
previousPosition = Int(item.position)
}
if let set = $1 as? DBSet {
currentPosition = Int(set.position)
}
if let item = $1 as? DBItem {
currentPosition = Int(item.position)
}
return previousPosition < currentPosition
}
}
position is type of Int16
How can I simplify that?
Create a protocol that defines your position property. Let's call it HasPosition. Declare an array of [HasPosition]. You can then sort the array directly with any of the standard sorting functions.
If .position returns the same type in both cases, and that type is comparable, you can simplify that to the following:
elements.sort {
let l = ($0 as? DBSet)?.position ?? ($0 as? DBItem)?.position
let r = ($1 as? DBSet)?.position ?? ($1 as? DBItem)?.position
return l < r
}
The key to this working is that there is a version of < that works for optionals, where nil is defined as less than any non-nil value. So values in the array not of one of the two types will be sorted to the beginning.
When you have the array as [AnyObject] and the position is Int16, this should works:
elements.sort {
($0.position as Int16?) < ($1.position as Int16?)
}
This works because AnyObject has any #objc properties as Optional.
Demo:
class DBItem:NSObject {
var position:Int16
init(position: Int16) { self.position = position }
override var description:String { return "DBItem(\(position))" }
}
class DBSet:NSObject {
var position:Int16
init(position: Int16) { self.position = position }
override var description:String { return "DBSet(\(position))" }
}
var elements:[AnyObject] = [
DBItem(position: 5),
DBSet(position: 2),
DBItem(position: 42),
DBSet(position: 62),
DBSet(position: 21),
DBItem(position: 6),
DBSet(position: 36),
DBItem(position: 24),
]
elements.sort {
($0.position as Int16?) < ($1.position as Int16?)
}
println(elements)
// -> [DBSet(2), DBItem(5), DBItem(6), DBSet(21), DBItem(24), DBSet(36), DBItem(42), DBSet(62)]
And your prepareElements() can be simplified as:
private func prepareElements() {
elements = []
if let ct = currentCategory {
elements += Array(ct.items) as [AnyObject]
elements += Array(ct.sets) as [AnyObject]
elements.sort {
($0.position as Int16?) < ($1.position as Int16?)
}
}
}