class GuitarString: UIView {
lazy var topBorder: UIView = {
let line = UIView()
line.translatesAutoresizingMaskIntoConstraints = false
return line
}()
lazy var bottomBorder: UIView = {
let line = UIView()
line.translatesAutoresizingMaskIntoConstraints = false
return line
}()
var lineWidth: CGFloat = 0.5
var lineColor: UIColor = .black
init(){
super.init(frame: .zero)
layout()
}
private func layout() {
self.addSubview(topBorder)
self.backgroundColor = .lightGray
topBorder.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
topBorder.topAnchor.constraint(equalTo: self.topAnchor).isActive = true
topBorder.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
topBorder.heightAnchor.constraint(equalToConstant: lineWidth).isActive = true
topBorder.backgroundColor = lineColor
self.addSubview(bottomBorder)
bottomBorder.leftAnchor.constraint(equalTo: self.leftAnchor).isActive = true
bottomBorder.bottomAnchor.constraint(equalTo: self.bottomAnchor).isActive = true
bottomBorder.rightAnchor.constraint(equalTo: self.rightAnchor).isActive = true
bottomBorder.heightAnchor.constraint(equalToConstant: lineWidth).isActive = true
bottomBorder.backgroundColor = lineColor
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
カテゴリー: Swift
class CustomUIView: UIView {
init() {
super.init(frame: .zero)
// any initialization comes here...
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
ステップ1: main Storyboardを削除

ステップ2: Main Interface にある”Main”を削除

ステップ3: Info.plistのUISceneStoryboardFileとMainの記述を削除

ステップ4: SceneDelegateのfunc scene() を以下のように記述
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
}
ステップ5: ViewControllerにコードを記述開始
import UIKit
class ViewController: UIViewController {
let bannerHeight: CGFloat = 100.0
var viewWidth: CGFloat {
view.bounds.width
}
var viewHeight: CGFloat{
view.bounds.height
}
var topInset: CGFloat {
view.safeAreaInsets.top
}
var bottomInset: CGFloat {
view.safeAreaInsets.bottom
}
var appAreaHeight: CGFloat {
viewHeight - topInset - bottomInset - bannerHeight
}
lazy var first: UIView = {
let view = UIView()
view.backgroundColor = .red
return view
}()
lazy var second: UIView = {
let view = UIView()
view.backgroundColor = .blue
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(first)
view.addSubview(second)
}
override func viewWillLayoutSubviews() {
print("topInset: \(topInset)")
print("bottomInset: \(bottomInset)")
first.frame = CGRect(x: 0, y: topInset + bannerHeight, width: viewWidth, height: appAreaHeight/2)
second.frame = CGRect(x: 0, y: topInset + bannerHeight + appAreaHeight/2, width: viewWidth, height: appAreaHeight/2)
}
}
@State var isLongPressed = false
var longPress: some Gesture {
LongPressGesture(minimumDuration: 3)
.onEnded { ges in
self.isLongPressed = true
}
}
ProductリストをUIに表示させるバックエンドフローは以下
// array of fetched product ids
var identifiers: [String] = []
// array of Product
var products: [Products] = []
// fetch from server and store data in Keychain
func fetchProductsFromServer() async {
let url = URL(string: "https://jamapp.me/test_getInAppProducts")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
KeychainWrapper.standard.set(data, forKey: dataKey)
} catch {
print(error)
}
}
// decode from keychain and fill identifier array
func decodeProductsFromKeychain() async {
guard let data = KeychainWrapper.standard.data(forKey: self.dataKey) else { return }
do {
let decode = try JSONDecoder().decode(ServerResponse.self, from: data)
decodedProducts = Set(decode.products)
for product in decode.products {
identifiers.insert(product.identifier)
}
} catch {
print(error)
}
}
// fetch from AppStore
func fetchProductsFromAppStore() async {
do {
products = try await Product.products(for: identifiers)
for product in products {
if await product.currentEntitlement != nil {
// storing currentEntitlement in Keychain for off-line use...
KeychainWrapper.standard.set(true, forKey: product.id)
// storing in isPurchasedDict to update UI
await MainActor.run {
self.isPurchasedDict[product.id] = KeychainWrapper.standard.bool(forKey: product.id)
}
} else {
KeychainWrapper.standard.set(false, forKey: product.id)
await MainActor.run {
self.isPurchasedDict[product.id] = KeychainWrapper.standard.bool(forKey: product.id)
}
}
}
} catch {
print(error)
}
}
in order to handle any unhandled transactions, listen for Transaction.updates in appDelegate
extension AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
// listening for transaction updates
Task {
await listenForTransactions()
}
return true
}
func listenForTransactions() async {
for await verificationResult in Transaction.updates {
switch verificationResult {
case .verified(let transaction):
KeychainWrapper.standard.set(true, forKey: transaction.productID)
await transaction.finish()
case .unverified(let transaction, _):
await transaction.finish()
}
}
}
}
in order to buy a product, do below.
func buyProduct(product: Product) {
Task {
do {
let result = try await product.purchase()
switch result {
case .success(let verification):
switch verification {
case .verified(let transaction):
await updatePurchase(transaction: transaction)
await transaction.finish()
case .unverified(let transaction, _):
await transaction.finish()
}
case .userCancelled:
()
case .pending:
()
@unknown default:
()
}
} catch {
print(error)
}
}
}
func updatePurchase(transaction: Transaction) async {
KeychainWrapper.standard.set(true, forKey: transaction.productID)
await MainActor.run {
isPurchasedDict[transaction.productID] = true
}
}
UIScrollViewをスクロールさせるためには、scrollViewにSubviewとしてもう一つUIViewをcontentsViewとして追加し、contentsView.frame.sizeをscrollView.contentSizeに教える必要があります。
import UIKit
class ViewController: UIViewController {
// MARK: - UI Components
lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
return scrollView
}()
lazy var contentsView: UIView = {
let contentsView = UIView()
return contentsView
}()
var labels: [UILabel] = []
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
addViews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupLayouts()
}
private func addViews() {
view.addSubview(scrollView)
scrollView.addSubview(contentsView)
}
// MARK: - Layouts
private func setupLayouts() {
// remove and clear labels
labels.forEach { label in
label.removeFromSuperview()
}
labels = []
// constraining scrollView to the view
scrollView.frame = CGRect(x: 0, y: 0, width: view.bounds.width/2, height: view.bounds.height/2)
scrollView.backgroundColor = .white
// to only allow vertical scroll, constrain the contentsView width to the scrollView width
contentsView.frame = CGRect(x: 0, y: 0, width: scrollView.bounds.width, height: 0)
// to get aprox. same fontSize when the screen rotates
var fontSize: CGFloat = 0
if view.bounds.width > view.bounds.height {
fontSize = view.bounds.height*0.5*0.2
} else {
fontSize = view.bounds.width*0.5*0.2
}
// adding labels to the contentsView
for i in 0..<50 {
let label = UILabel()
label.frame = CGRect(x: 0, y: CGFloat(i)*fontSize, width: contentsView.bounds.width, height: fontSize)
label.font = UIFont(name: "HelveticaNeue-CondensedBold", size: fontSize)
label.text = "Label \(i)"
label.textColor = .black
contentsView.addSubview(label)
// retaining lables for removing from its superView for when screen rotates, etc...
labels.append(label)
}
// adjusting the contentsView height to the number of lables added
contentsView.frame.size = CGSize(width: scrollView.bounds.width, height: fontSize*50)
// to tell the scrollView the frame.size of the contentsView is important, otherwise will not scroll
scrollView.contentSize = contentsView.frame.size
}
}
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)
}
}
}
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)
}
}
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)
}
}
この記事とこの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” エラーが発生します。
