カテゴリー
iOS Swift

UIBezierPathのaddArcの使い方

addArc(withCenter: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)

addArchのstartAngelとendAngleは円上のradianを指定しますが、centerをcとすると、0と2piは下記の位置になります。そのため、円の一番上の頂点から円を描こうとすると、そのstartAngleは-CGFloat.pi/2とする必要があります。

        -pi/2
          |
          |
          |
----------c---------- 0 and 2pi
          |
          |
          |
import UIKit

class ViewController: UIViewController {

  var viewHeight: CGFloat {
    view.bounds.height
  }
  var viewWidth: CGFloat {
    view.bounds.width
  }
  var topInset: CGFloat {
    view.safeAreaInsets.top
  }
  var bottomInset: CGFloat {
    view.safeAreaInsets.bottom
  }
  var appAreaHeight: CGFloat {
    viewHeight - topInset - bottomInset
  }
  var diameter: CGFloat {
    if viewWidth > viewHeight {
      return viewHeight*0.4
    } else {
      return viewWidth*0.4
    }
  }
	
  var rotated: Bool = false
	
  override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view.		
  }
	
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    drawPieChart()
  }
	
  // this is called when rotate the screen
  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    print("viewWillTransition called")
    rotated = true
  }
	
  override func viewWillLayoutSubviews() { // view lifecycle [3]
    super.viewWillLayoutSubviews()
    print("viewWillLayoutSubviews called")
    if rotated {
      drawPieChart()
    }
  }

  // test values for drawing a pieChart
  var values: [Int:Int] = [1:2, 2:2, 3:1]
	
  // to start drawing the arc from top of the circle
  let initialRadianForArc = -CGFloat.pi/2
  // to pass the starting point of the arc to next iteration
  var nextRadianForArc: CGFloat = 0

  // need to store these for clearing paths and layers when the screen rotates and transitions	
  var paths: [UIBezierPath] = []
  var layers: [CAShapeLayer] = []
	
  private func drawPieChart() {
    // need to remove layers from its superLayer and clear the array
    layers.forEach { layer in
      layer.removeFromSuperlayer()
    }
    layers = []
    paths = []
		
    var totalValue = values.map({ $0.value }).reduce(0, +)
    let pi2 = 2*CGFloat.pi
    let center = CGPoint(x: viewWidth/2, y: topInset + appAreaHeight/2)
		
    for (i, dic) in values.enumerated() {
      if i == 0 {
        let path = UIBezierPath()
        path.move(to: center)
        let theta = pi2*(CGFloat(dic.value)/CGFloat(totalValue))
        let startAngle = initialRadianForArc
        let endAngle = startAngle + theta
        nextRadianForArc = endAngle
        path.addArc(withCenter: center, radius: diameter/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        path.move(to: center)
        paths.append(path)
      } else {
        let path = UIBezierPath()
        path.move(to: center)
        let theta = pi2*(CGFloat(dic.value)/CGFloat(totalValue))
        let startAngle = nextRadianForArc
        let endAngle = startAngle + theta
        nextRadianForArc = endAngle
        path.addArc(withCenter: center, radius: diameter/2, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        path.move(to: center)
        paths.append(path)
      }
    }

    // in case you want to animate the opacity...		
    let animation = CABasicAnimation(keyPath: "opacity")
    animation.fromValue = 0
    animation.toValue = 1
    animation.duration = 1.2
		
    paths.forEach{ path in
      let shapeLayer = CAShapeLayer()
      let red = CGFloat.random(in: 0...255)
      let green = CGFloat.random(in: 0...255)
      let blue = CGFloat.random(in: 0...255)
      shapeLayer.path = path.cgPath
      shapeLayer.lineWidth = 3
      shapeLayer.lineCap = .round
      shapeLayer.lineJoin = .round
      shapeLayer.strokeColor = UIColor.systemGray.cgColor
      shapeLayer.fillColor = UIColor(red: red/255, green: green/255, blue: blue/255, alpha: 1).cgColor
      shapeLayer.add(animation, forKey: "opacity")
      layers.append(shapeLayer)
      view.layer.addSublayer(shapeLayer)
    }
  }
}
カテゴリー
iOS Swift

TabView遷移と画面回転時のアニメーション

TabBarのボタンで他のTabViewに移動し戻ってきた時のアニメーションと、画面回転時のアニメーションとでは、コールするメソッドが違うので少し苦労しました。Viewのライフサイクルに合わせて調整が必要でした。

画面回転、TabView遷移の際のView Lifecycleは以下のようになっていました。

== first launch ==

viewDidLoad called
viewWillAppear called
viewWillLayoutSubviews called *
viewDidLayoutSubviews called *
viewDidAppear called

== rotated to landscape ==

viewWillTransition called
viewWillLayoutSubviews called *
viewDidLayoutSubviews called *

== rotated to portrait ==

viewWillTransition called
viewWillLayoutSubviews called *
viewDidLayoutSubviews called *

== went to another tab ==

viewDidDisappear called

== came back from another tab view ==

viewWillAppear called
viewWillLayoutSubviews called *
viewDidLayoutSubviews called *
viewDidAppear called

import UIKit
import CoreData

class StatisticsIntervalVC: UIViewController {
	
  // this gets updated from records in Core Data in viewWillAppear
  var intervals: [StatisticInterval] = []

  // if true, will trigger the animation in viewWillLayoutSubviews() and viewDidLayoutSubviews() methods...	
  var rotated: Bool = false
	
  let initialChartValue: CGFloat = 0.6

  // some Core Data setups...	
  let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext
  let request = Record.fetchRequest()
  var fetchedData: [Record]?
	
  var viewHeight: CGFloat {
    view.bounds.height
  }
	
  var viewWidth: CGFloat {
    view.bounds.width
  }
	
  var diameter: CGFloat {
    if viewHeight > viewWidth {
      return viewWidth*0.7
    } else {
      return viewHeight*0.7
    }
  }
	
  // CALayer to draw dots and lines in
  let dotLayer = CAShapeLayer()
  let lineLayer = CAShapeLayer()
	
  // initial path for dots and lines. these are needed to make animations.
  var oldDotPath = UIBezierPath()
  var oldLinePath = UIBezierPath()
	
  override func viewDidLoad() { // view lifecycle [1]
    super.viewDidLoad()
    // filling intervals array
    Interval.allCases.forEach { _ in
      let interval = StatisticInterval(count: 0, correctRate: 0, triedAnswers: [], timeTookAverage: 0)
      intervals.append(interval)
    }
    // fetching data from Core Data. This happens only once during the lifecycle of this view (including rotation and going to another tab view and come back.
    fetchData(request: request)
    iterate()
    print("viewDidLoad called")
  }
	
  override func viewWillAppear(_ animated: Bool) { // view lifecycle [2]
    super.viewWillAppear(animated)
    // this is called when the view will appear via the selector button
    drawInitialLines()
    print("viewWillAppear called")
}
	
  override func viewWillLayoutSubviews() { // view lifecycle [3]
    super.viewWillLayoutSubviews()
    print("viewWillLayoutSubviews called")
    if rotated {
      drawInitialLines()
    }
  }
	
  override func viewDidLayoutSubviews() { // view lifecycle [4]
    super.viewDidLayoutSubviews()
    print("viewDidLayoutSubviews called")
    if rotated {
      DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
        self?.updateLines()
      }
    }
    // resetting this to false
    rotated = false
  }
	
  override func viewDidAppear(_ animated: Bool) { // view lifecycle [5]
    super.viewDidAppear(animated)
    // this is called when coming back from another tab view
    // Is it ok to call asyncAfter on the main thread? It appears to be ok... 26 Jun 2022
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
      self?.updateLines()
    }
    print("viewDidAppear called")
  }
	
  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    print("viewWillTransition called")
    rotated = true
  }
	
  override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    // this is called when transitioned to another tab view
    print("viewDidDisappear called")
  }
	
  // Core Data
  private func fetchData(request: NSFetchRequest<Record>) {
    do {
      fetchedData = try context.fetch(request)
    } catch {
      print(error)
    }
  }
	
	// test func. to be replaced by a better business logic and ... name?
  private func iterate() {
    guard let fetchedData = fetchedData else {
      print("not fetched data available1")
      return
    }
		
    for (i, interval) in Interval.allCases.enumerated() {
      // intervalRecord includes all record for the interval
      let intervalRecord = fetchedData.filter{ $0.correctAnswer == interval.rawValue }
      // obtaining all triedAnswer
      var timeTook: Int32 = 0
      intervalRecord.forEach { record in
        timeTook += record.timeTook
        do {
          // converting back to [Int32] from Data
          if let unwrappedTriedAnswers = record.triedAnswers {
            let intArray: [Int32] = try JSONDecoder().decode([Int32].self, from: unwrappedTriedAnswers)
            intArray.forEach { answer in
              guard let interval = Interval(rawValue: answer) else {
                print("could not convert Int32 to Interval")
                return
              }				     
              intervals[i].triedAnswers.insert(interval)
            }
          }
        } catch {
          print(error)
        }
      }
      // counting how many data there is for the interval
      let count = intervalRecord.count
      // getting only the records with isCorrect == true
      let isCorrect = intervalRecord.filter{ $0.isCorrect }
      let isCorrectCount = isCorrect.count
      intervals[i].count = count
      intervals[i].correctRate = Float(isCorrectCount) / Float(count)
      intervals[i].timeTookAverage = Float(timeTook) / Float(count)
    }
  }
	
  private func updateLines() {
    // for lines
    let linePath = UIBezierPath()
		
    // for dots
    let dotPath = UIBezierPath()
    let dotRadius: CGFloat = 5
		
    let centerX = viewWidth/2
    let centerY = viewHeight/2
		
    var initialPoint: CGPoint = .zero
		
    for i in 0..<Interval.allCases.count {
      let index = CGFloat(i)
      let thetaOffset: CGFloat = CGFloat.pi*2/12
      var value = CGFloat(intervals[i].correctRate)
      if value.isNaN {
        value = initialChartValue
      }
      let point = CGPoint(x: centerX + cos(thetaOffset*index)*value*diameter/2, y: centerY - sin(thetaOffset*index)*value*diameter/2 )
      if i == 0 {
        initialPoint = point
        linePath.move(to: point)
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
      } else {
        linePath.addLine(to: point)
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
      }
    }
		
    // add line to the initial starting point
    linePath.addLine(to: initialPoint)
		
    let lineAnimation = CABasicAnimation(keyPath: "path")
    let dotAnimation = CABasicAnimation(keyPath: "path")
		
    lineAnimation.fromValue = oldLinePath.cgPath
    lineAnimation.toValue = linePath.cgPath
    lineAnimation.duration = 1.3
		
    dotAnimation.fromValue = oldDotPath.cgPath
    dotAnimation.toValue = dotPath.cgPath
    dotAnimation.duration = 1.3
		
    // I think this needs to be called on the main thread
    lineLayer.add(lineAnimation, forKey: "path")
    dotLayer.add(dotAnimation, forKey: "path")
		
    lineLayer.path = linePath.cgPath
    dotLayer.path = dotPath.cgPath
  }
	
  private func drawInitialLines() {
    lineLayer.removeFromSuperlayer()
    dotLayer.removeFromSuperlayer()
    // for lines
    let linePath = UIBezierPath()
		
    // for dots
    let dotPath = UIBezierPath()
    let dotRadius: CGFloat = 5
		
    let centerX = viewWidth/2
    let centerY = viewHeight/2
		
    var initialPoint: CGPoint = .zero
		
    for i in 0..<Interval.allCases.count {
      let index = CGFloat(i)
      let thetaOffset: CGFloat = CGFloat.pi*2/12
      let point = CGPoint(x: centerX + cos(thetaOffset*index)*initialChartValue*diameter/2,
													y: centerY - sin(thetaOffset*index)*initialChartValue*diameter/2 )
      if i == 0 {
        initialPoint = point
        linePath.move(to: point)
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
      } else {
        linePath.addLine(to: point)
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
      }
    }
		
    // add line to the initial starting point
    linePath.addLine(to: initialPoint)
		
    oldDotPath = dotPath
    oldLinePath = linePath
		
    let animation = CABasicAnimation(keyPath: "opacity")
    animation.fromValue = 0
    animation.toValue = 1
    animation.duration = 1.3
		
    dotLayer.fillColor = UIColor.systemPink.cgColor
    dotLayer.lineWidth = 3
    dotLayer.path = dotPath.cgPath
    dotLayer.add(animation, forKey: "opacity")
		
    lineLayer.strokeColor = UIColor.systemCyan.cgColor
    lineLayer.fillColor = UIColor.clear.cgColor
    lineLayer.lineWidth = 3
    lineLayer.lineJoin = .round
    lineLayer.lineCap = .round
    lineLayer.path = linePath.cgPath
    lineLayer.add(animation, forKey: "opacity")
		
    view.layer.addSublayer(lineLayer)
    view.layer.addSublayer(dotLayer)
  }
}
カテゴリー
iOS Swift

Core AnimationでChartをアニメーションする

import UIKit

class ViewController: UIViewController {
  // dummy y values
  let yValue: [CGFloat] = [3.0, 2, 5, 6, 1, 3]	

  // bezierPath to use as the fromValue for animation
  var oldDotPath = UIBezierPath()
  var oldLinePath = UIBezierPath()  

  // basic computed properties
  var topInset: CGFloat {
    viewHeight/2 - appAreaHeight/2
  }

  var xOffset: CGFloat {
    appAreaWidth/CGFloat(yValue.count-1)
  }
	
  var dotX: CGFloat {
    viewWidth/2 - appAreaWidth/2
  }

  var rate: CGFloat {
    appAreaHeight/yValue.max()!
  }

  var viewHeight: CGFloat {
		view.bounds.height
  }
  
  var viewWidth: CGFloat {
    view.bounds.width
  }

  var appAreaHeight: CGFloat {
    viewHeight*0.8
  }
	
  var appAreaWidth: CGFloat {
    viewWidth*0.9
  }
	
  let dotLayer = CAShapeLayer()
  let lineLayer = CAShapeLayer()
	
  lazy var button : UIButton = {
    let button = UIButton()
    button.setTitle("tap", for: .normal)
    button.setTitleColor(.blue, for: .normal)
    button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
    return button
  }()
	
  override func viewDidLoad() {
    super.viewDidLoad()
    addToView()
    drawInitialLines()
  }
	
  override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    layoutButton()
  }
	
  @objc func buttonTapped() {
    updatePathValues()
  }
	
  private func addToView() {
    view.addSubview(button)
  }
	
  private func layoutButton() {
    let buttonWidth: CGFloat = 100
    let buttonHeight: CGFloat = 50
    let buttonX = viewWidth/2 - buttonWidth/2
    let buttonY = viewHeight/2 + appAreaHeight/2 - buttonHeight
    button.frame = CGRect(x: buttonX, y: buttonY, width: buttonWidth, height: buttonHeight)
  }
	
  private func updatePathValues() {
    let linePath = UIBezierPath()
		
      // for dots
    let dotPath = UIBezierPath()
    let dotRadius: CGFloat = 5
		
    for i in 0..<yValue.count {
      let point = CGPoint(x: dotX + xOffset*CGFloat(i), y: topInset+appAreaHeight-rate*yValue[i])
      if i == 0 {
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        linePath.move(to: point)
      } else {
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        linePath.addLine(to: point)
      }
    }
		
    let dotAnimation = CABasicAnimation(keyPath: "path")
    dotAnimation.fromValue = oldDotPath.cgPath
    dotAnimation.toValue = dotPath.cgPath
    dotAnimation.duration = 0.9
		
    let lineAnimation = CABasicAnimation(keyPath: "path")
    lineAnimation.fromValue = oldLinePath.cgPath
    lineAnimation.toValue = linePath.cgPath
    lineAnimation.duration = 0.9
	
    // add animation to the layers	
    dotLayer.add(dotAnimation, forKey: "path")
    lineLayer.add(lineAnimation, forKey: "path")
	
    // persisting path changes by assigning the info	
    dotLayer.path = dotPath.cgPath
    lineLayer.path = linePath.cgPath
  }
	
  private func drawInitialLines() {
    // for lines
    let linePath = UIBezierPath()
		
    // for dots
    let dotPath = UIBezierPath()
    let dotRadius: CGFloat = 5
	
    // drawing lines and dots that fit dummy y values	
    for i in 0..<yValue.count {
      let point = CGPoint(x: dotX + xOffset*CGFloat(i), y: topInset+appAreaHeight/2)
      if i == 0 {
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        linePath.move(to: point)
      } else {
        dotPath.move(to: point)
        dotPath.addArc(withCenter: point, radius: dotRadius, startAngle: 0, endAngle: 2*CGFloat.pi, clockwise: true)
        linePath.addLine(to: point)
      }		
    }
		
    // retaining the paths for the later use in animation
    oldDotPath = dotPath
    oldLinePath = linePath
		
    dotLayer.fillColor = UIColor.systemPink.cgColor
    dotLayer.lineWidth = 3
    // setting the path here. by changing the path, can animate the dots.
    dotLayer.path = dotPath.cgPath
		
    lineLayer.strokeColor = UIColor.systemCyan.cgColor
    lineLayer.fillColor = UIColor.clear.cgColor 
    lineLayer.lineWidth = 3
    lineLayer.lineJoin = .round
    lineLayer.lineCap = .round
    // setting the path here...
    lineLayer.path = linePath.cgPath
	
    // adding these layers to the view's backing layer	
    view.layer.addSublayer(lineLayer)
    view.layer.addSublayer(dotLayer)
  }
}
カテゴリー
iOS Swift

CoreDataを学ぶ

この記事とこのYoutubeプレイリストはとても参考になります。

NSManagedObjectはCoreDataのDataModelのあらゆるEntityを表すことが可能です。

下記のようにPerson Entityのname AttributeへString型を格納出来ます。

// appDelegateをゲット
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }

// managedContextをゲット。その後はmanagedContextを介して, KVCを使ってCore Dataにsaveできる		
let managedContext = appDelegate.persistentContainer.viewContext
		
// you can write the above in one line
// let managedContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

let entity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)!
let person = NSManagedObject(entity: entity, insertInto: managedContext)
person.setValue(name, forKey: "name")
		
do {
  try managedContext.save()
} catch {
  print(error)
}

Core DataからのFetchは以下のように出来ます。これをviewDidLoad内などで必要に応じて呼び出します。

guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
  
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")

do {
  // returns an array of [NSManagedObject]
  people = try managedContext.fetch(fetchRequest)
} catch {
  print(error)
}

tableViewではこのようにBinding出来ます。

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let person = people[indexPath.row]
  let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
  cell.textLabel?.text = person.value(forKeyPath: "name") as? String
  return cell
}

ただ、この仕組みをもっと効率よく(Object-Orientedに)書くことが出来ます。

RecordというEntityがあり、そのattributeとしてname: Stringがあるとします。

EntityのCodegenは Manual/Noneを選択し、Editor -> Create NSManagedObject subclass で以下のようにsubclassを作成出来ます。

import Foundation
import CoreData

@objc(Record)
  public class Record: NSManagedObject {
}

extension Record {
  @nonobjc public class func fetchRequest() -> NSFetchRequest<Record> {
  return NSFetchRequest<Record>(entityName: "Record")
}
  @NSManaged public var name: String
}

extension Record : Identifiable {

}
// get context
let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

// create new record
let newRecord = Record(context: context)
let newName = "Your Name"
newRecord.name = newName

do {
  try context.save()
} catch {
  print(error)
}

// to fetch
let request: NSFetchRequest<Record> = Record.fetchRequest()
do {
  // this returns an array of NSManagedObject
  let result = try self.context.fetch(request)
} catch {
  print(error)
}

To Manyとするオブジェクトの順番をCore Dataに保存する場合はOrderedにチェックを入れます。

非標準のオブジェクト(Int配列など)をCore Dataに格納したい場合は下記のように一旦Dataとして格納し(Attribute type は Binary Dataを選択)、使用時にはオブジェクトに戻して使う、などの方法があります。

// to save
let newRecord = Record(context: self.context)
let intArray = [1,2,3]
let arrayAsString = intArray.description
let stringAsData = arrayAsString.data(using: String.Encoding.utf8)
newRecord.arrayData = stringAsData

// to fetch
let request: NSFetchRequest<Record> = Record.fetchRequest()
do {
  let result = try self.context.fetch(request)
  let intArray: [Int] = try JSONDecoder().decode([Int].self, from result.first!.arrayData!)
} catch {
  print(error)
}

複合数のpredicateを組み合わせてcoredataを検索したり、objectを削除したりする場合は下記のようにします。

let predicateNote = NSPredicate(format: "note == %@", note.name)
let predicateID = NSPredicate(format: "chordID == %@" , NSNumber(value: id))
let predicateRootOrTop = NSPredicate(format: "rootOrTop == %@", rootOrTop.name)
let predicateFret = NSPredicate(format: "targetNoteFret == %@", NSNumber(value: fret))
let compoundPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicateNote, predicateID, predicateRootOrTop, predicateFret])
		
request.predicate = compoundPredicate
		
do {
  let results = try managedContext.fetch(request)
  if results.count == 0 {
    let newBookmark = Bookmarked(context: managedContext)
    newBookmark.note = note.name
    newBookmark.chordID = Int32(id)
    newBookmark.rootOrTop = rootOrTop.name
    newBookmark.targetNoteFret = Int32(fret)
    try managedContext.save()
  } else {
    let bookmark = results[0]
    managedContext.delete(bookmark)
    try managedContext.save()
  }
} catch {
  print(error)
}

検索結果をソートしたい場合はSort Descriptorを使います。

let sort = NSSortDescriptor(key "name", ascending: true)
let sort = NSSortDescriptor(key "name", ascending: true, selector: "customSort:")
let sort = NSSortDescriptor(key: "name", ascending: true, comparator: { (a, b) -> NSComparisonResult in
 return .OrderAscending
})

fetchRequest.sortDescriptors = [sort]

オブジェクトを削除する際、そのオブジェクトにRelationshipがある場合、delete optionを適切に選ぶ必要があります。例えばownerとdeviceというEntityがあり、ownerがdevice(s)を所有しているrelationshipがあるとします。ownerオブジェクトを削除する場合、deviceのownerに対するrelationshipをどうするのか、以下の4つから選択します。

Nullify – deviceのownerはnilとする

Cascade – device(s)オブジェクトも削除する

Deny – ownerとdevice(s)にrelationshipが存在する場合は削除できない

No Action – プログラマが独自のロジックを用いたい場合

DataModelに変更を加える場合はEditor -> Add Model Versionを選択し、直前のversionから新しいmodelを作成します。

正しいModel versionを選択しないと”An NSManagedObject of class must have a valid NSEntityDescription” エラーが発生します。

カテゴリー
iOS Swift

View LifecycleのどのタイミングでboundsとsafeAreaInsetsの情報を得るか

override func viewDidLoad() {
  super.viewDidLoad()
  print("printing view.bounds in viewDidLoad: \(view.bounds)")
  print("printing view.safeAreaInsets in viewDidLoad: \(view.safeAreaInsets)")
}

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  print("printing view.bounds in viewWillAppear: \(view.bounds)")
  print("printing view.safeAreaInsets in viewWillAppear: \(view.safeAreaInsets)")
}

override func viewWillLayoutSubviews() {
  super.viewWillLayoutSubviews()
  print("printing view.bounds in viewWillLayoutSubviews: \(view.bounds)")
  print("printing view.safeAreaInsets in viewWillLayoutSubviews: \(view.safeAreaInsets)")
}
	
override func viewDidLayoutSubviews() {
  super.viewDidLayoutSubviews()
  print("printing view.bounds in viewWillLayoutSubviews: \(view.bounds)")
  print("printing view.safeAreaInsets in viewWillLayoutSubviews: \(view.safeAreaInsets)")
}

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)
  print("printing view.bounds in viewDidAppear: \(view.bounds)")
  print("printing view.safeAreaInsets in viewDidAppear: \(view.safeAreaInsets)")
}

結果

printing view.bounds in viewDidLoad: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewDidLoad: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)

printing view.bounds in viewWillAppear: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewWillAppear: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)

printing view.bounds in viewWillLayoutSubviews: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewWillLayoutSubviews: UIEdgeInsets(top: 48.0, left: 0.0, bottom: 34.0, right: 0.0)

printing view.bounds in viewWillLayoutSubviews: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewWillLayoutSubviews: UIEdgeInsets(top: 48.0, left: 0.0, bottom: 34.0, right: 0.0)

printing view.bounds in viewDidAppear: (0.0, 0.0, 414.0, 896.0)

printing view.safeAreaInsets in viewDidAppear: UIEdgeInsets(top: 48.0, left: 0.0, bottom: 34.0, right: 0.0)

カテゴリー
iOS Swift

iOSで使用可能なFontを表示する

func getFontInfo() {
  let fontFamily = UIFont.familyNames
  for i in 0..<fontFamily.count {
    let family = fontFamily[i]
    let fontName = UIFont.fontNames(forFamilyName: family)
    print("\(family) : \(fontName)")
  }
}