Skip to content

๐Ÿ“ CoreData, Dropbox๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋™๊ธฐํ™” ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋ชจ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ ์•ฑ

Notifications You must be signed in to change notification settings

leeari95/ios-cloud-notes

ย 
ย 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

68 Commits
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐Ÿ“ ๋™๊ธฐํ™” ๋ฉ”๋ชจ์žฅ ํ”„๋กœ์ ํŠธ

  • ํŒ€ ํ”„๋กœ์ ํŠธ(3์ธ)
  • ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„: 2022.02.07 - 2022.02.25

๋ชฉ์ฐจ

ํ‚ค์›Œ๋“œ

  • UISplitViewController
  • DateFormatter Locale TimeZone
  • UITapGestureRecognizer
  • subscript Collection
  • SceneDelegate
  • NavigationItem UIBarButtonItem
  • UITextView
    • typingAttributes
    • UITextViewDelegate
  • Core Data NSPersistentCloudKitContainer NSEntityDescription
    • NSFetchRequest NSPredicate NSSortDescriptor
    • NSManagedObject
  • NSMutableAttributedString
  • UIActivityViewController UIAlertController
    • popoverPresentationController
  • UITableView
    • UISwipeActionsConfiguration UIContextualAction
    • insertRows selectRow deleteRows
    • UITableViewCell
      • setSelected selectedBackgroundView
  • viewWillTransition
  • UIFont
    • UIFontMetrics UIFontDescriptor
  • flatMap
  • Swift Package Manager
  • SwifryDropbox
    • DispatchGroup
    • FileManager
  • UIActivityIndicatorView
  • UISearchController
  • Localization
    • NSLocalizedString
  • Accessibility Dynamic Type VoiceOver
    • Accessibility Inspector
  • Lite Mode Dark Mode

โญ๏ธ ํ”„๋กœ์ ํŠธ ์†Œ๊ฐœ

ํด๋ผ์šฐ๋“œ ์„œ๋ฒ„์™€ ๋™๊ธฐํ™” ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋ชจ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ ์•ฑ์ด์—์š”. โ˜๏ธ

์•„์ดํฐ ๋ฐ ์•„์ดํŒจ๋“œ์˜ ๊ธฐ๋ณธ์•ฑ์ธ ๋ฉ”๋ชจ ์•ฑ๊ณผ ํก์‚ฌํ•œ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์š”. ๐Ÿ“

CoreData๋ฅผ ํ†ตํ•ด ๊ฐ ๋””๋ฐ”์ด์Šค ๋ณ„๋กœ ๋ฉ”๋ชจ ๋ฐ์ดํ„ฐ๋ฅผ ์˜๊ตฌ์ ์œผ๋กœ ์ €์žฅํ•  ์ˆ˜ ์žˆ์–ด์š”. ๐Ÿ“„


โœจ ํ”„๋กœ์ ํŠธ ์ฃผ์š”๊ธฐ๋Šฅ

๐Ÿ“ฆ ์ฒ˜์Œ ์‹คํ–‰ ์‹œ ๋“œ๋กญ๋ฐ•์Šค ์—ฐ๋™ํ• ๊ฑด์ง€์— ๋Œ€ํ•œ ํ™”๋ฉด์ด ๋œน๋‹ˆ๋‹ค.

โŒ ์—ฐ๋™์— ์„ฑ๊ณตํ•˜๋ฉด ๋ฐฑ์—…ํ–ˆ๋˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋ฉ๋‹ˆ๋‹ค. ๋กœ๋“œ๋˜๋Š” ๋™์•ˆ์—๋Š” ํ™”๋ฉดํ„ฐ์น˜๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ‡บ๐Ÿ‡ธ ์ง€์—ญํ™” ๊ตฌํ˜„์ด ๋˜์–ด์žˆ์–ด, ํ•œ๊ธ€ ์™ธ์—๋„ ์˜์–ด๋„ ์ง€์›ํ•˜๋Š” ์•ฑ์ž…๋‹ˆ๋‹ค.

๐ŸŒ— ๋ผ์ดํŠธ๋ชจ๋“œ, ๋‹คํฌ๋ชจ๋“œ๋ฅผ ์ง€์›ํ•ด์š” !

๐Ÿ“ ๋ฉ”๋ชจ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜, ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์–ด์š”.

โœจ ์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์„ ๊ตฌ๋ถ„ํ•ด ํ…์ŠคํŠธ ํฌ๊ธฐ๊ฐ€ ๋ณ€ํ™”ํ•˜๊ณ , ๊ทธ์—๋”ฐ๋ผ ์ขŒ์ธก ์…€์˜ ๋‚ด์šฉ๋„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜์š”.

๐Ÿ” ์„œ์น˜๋ฐ”๋ฅผ ์ด์šฉํ•ด ์›ํ•˜๋Š” ๋ฉ”๋ชจ๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿ—ƒ ๋ฉ”๋ชจ์˜ ๋‚ด์šฉ์„ ๊ณต์œ ํ•  ์ˆ˜๋„ ์žˆ์–ด์š” !


๐Ÿ’ช๐Ÿป ํ”„๋กœ์ ํŠธ ๊ธฐ์ˆ  ์Šคํƒ

UI Local Remote
UIKit CoreData SwifryDropbox

๐Ÿ›  Trouble Shooting

"๋ฉ”๋ชจ์žฅ์— ํ…์ŠคํŠธ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ Crash๋‚˜๋Š” ๋ฌธ์ œ"

  • ์ƒํ™ฉ ๋ฉ”๋ชจ์žฅ์— linebreak๊ฐ€ 1๊ฐœ์ผ ๋•Œ Crash๊ฐ€ ๋‚˜๋Š” ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚ฌ๋‹ค. ์•„๋ž˜๋Š” ๋ชจ๋“  ๋ฉ”๋ชจ๋ฅผ ์ง€์› ์„ ๊ฒฝ์šฐ Crash๊ฐ€ ๋‚˜๋Š” ์ƒํ™ฉ์ด๋‹ค.
  • ์ด์œ  ๋ฐฐ์—ด์„ ์กฐํšŒํ•  ๋•Œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ธ๋ฑ์Šค๋ฅผ ์กฐํšŒํ•  ๊ฒฝ์šฐ ์•ฑ์ด ์ฃฝ์–ด๋ฒ„๋ฆฌ๋Š” ์ƒํ™ฉ์ด์˜€๋˜ ๊ฒƒ์ด๋‹ค.
  • ํ•ด๊ฒฐ ๋”ฐ๋ผ์„œ ์ธ๋ฑ์Šค๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์กฐํšŒํ•˜๋„๋ก subscript๋ฅผ extension ํ•ด์ฃผ์–ด ์กฐํšŒ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๋Œ€์ฒ˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด๊ฒฐํ•˜์˜€๋‹ค.
    extension Collection {
        subscript (safe index: Index) -> Element? {
            return indices.contains(index) ? self[index] : nil
        }
    }

"iPad์—์„œ UIAlertController์˜ actionSheet ์‚ฌ์šฉ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜"

์˜ค๋ฅ˜๋ฉ”์„ธ์ง€

  • UIActivityViewController๋ฅผ present๋ฅผ ํ•ด์ฃผ๋ ค๋Š”๋ฐ ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๋ฉ”์„ธ์ง€๊ฐ€ ๋–ด๋‹ค.
Thread 1: "Your application has presented a UIAlertController (<UIAlertController: 0x10d813a00>) of style UIAlertControllerStyleActionSheet from CloudNotes.SplitViewController (<CloudNotes.SplitViewController: 0x11f7068f0>).
The modalPresentationStyle of a UIAlertController with this style is UIModalPresentationPopover. 
You must provide location information for this popover through the alert controller's popoverPresentationController.
You must provide either a sourceView and sourceRect or a barButtonItem. 
If this information is not known when you present the alert controller, you may provide it in the UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation."
  • ๊ฐ„๋‹จํžˆ ํ•ด์„ํ•˜์ž๋ฉด iPad์—์„œ ์•ก์…˜์‹œํŠธ๋ฅผ present๋ฅผ ํ•  ๊ฒฝ์šฐ ๋ชจ๋‹ฌ์Šคํƒ€์ผ์ด UIModalPresentationPopover์ด๊ณ , ์ด๊ฑธ ์‚ฌ์šฉํ•  ๋•Œ๋Š” barButtonItem ๋˜๋Š” ํ•ด๋‹น ์ฐฝ์˜ ๋Œ€ํ•œ ์œ„์น˜๋ฅผ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค๊ณ  ๋˜์–ด์žˆ๋‹ค.
  • ๋”ฐ๋ผ์„œ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๊ฒƒ์€ 2๊ฐ€์ง€์ค‘ ํ•˜๋‚˜์ด๋‹ค.
    • ํ•„์ˆ˜์ ์œผ๋กœ sourceView ์ง€์ •ํ•ด์ฃผ๊ธฐ
    • popoverPresentationController์— sourceRect ๋˜๋Š” barButtonItem ํ• ๋‹นํ•ด์ฃผ๊ธฐ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

์–ผ๋Ÿฟ์„ present ํ•ด์ฃผ๊ธฐ ์ „์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ if๋ฌธ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์ž!

  • UIBarButtonItem์— ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•
// UIViewController extension ๋‚ด๋ถ€...
if let popoverController = activityViewController.popoverPresentationController {
    popoverController.sourceView = self.splitViewController?.view
    popoverController.barButtonItem = sender // ๋ฉ”์†Œ๋“œ ๋‚ด๋ถ€๋ผ์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ barButtonItem ์ „๋‹ฌ๋ฐ›์•„ ํ• ๋‹นํ•ด์ฃผ์—ˆ๋‹ค.
}
  • ์œ„์น˜๋ฅผ ์ •ํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•
if let popoverController = activityViewController.popoverPresentationController {
    popoverController.sourceView = self.splitViewController?.view // presentํ•  ๋ทฐ ์ง€์ •
    popoverController.sourceRect = CGRect( // ๋ทฐ์˜ ์ • ๊ฐ€์šด๋ฐ ์œ„์น˜๋กœ ์ง€์ •
        x: self.splitViewController?.view.bounds.midX,
        y: self.splitViewController?.view.bounds.midY,
        width: 0,
        height: 0
    )
    popoverController.permittedArrowDirections = [] // ํ™”์‚ดํ‘œ๋ฅผ ๋นˆ๋ฐฐ์—ด๋กœ ๋Œ€์ž…
}

์˜๋ฌธ์ 

์œ„์น˜๋ฅผ ์ง€์ •ํ•˜๊ณ  ๋‚˜์„œ ํ™”๋ฉด์„ ๋Œ๋ ธ๋Š”๋ฐ... ๊ฐ€์šด๋ฐ ์œ„์น˜์— ์•ˆ์žˆ๊ณ  ์š”์ƒํ•œ ๊ณณ์— ์žˆ๋‹ค....

  • ํ•ด๊ฒฐ๋ฐฉ๋ฒ•
  • ํ™”๋ฉด์ด ๋Œ์•„๊ฐˆ ๋•Œ๋งˆ๋‹ค ํฌ์ง€์…˜์„ ๋‹ค์‹œ ์žก์•„์ฃผ๋ฉด ๋œ๋‹ค. ๊ทธ๊ฑธ ์œ„ํ•ด viewWillTransition ๋ฉ”์†Œ๋“œ๋ฅผ ํ™œ์šฉํ•ด๋ณด๊ฒ ๋‹ค.
    • ์ด ๋ฉ”์†Œ๋“œ๋Š” ViewController์˜ ๋ทฐ ํฌ๊ธฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์ „์— ํ˜ธ์ถœ์ด ๋œ๋‹ค.
  • ์ผ๋‹จ ์–ผ๋Ÿฟ์„ presentํ•˜๋Š” ๋ทฐ์— popoverController๋ผ๋Š” ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.
class SplitViewController: UISplitViewController {
    ...
    var popoverController: UIPopoverPresentationController?
  • ๊ทธ๋ฆฌ๊ณ  viewWillTransition ๋ฉ”์†Œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜์—ฌ ์œ„์น˜๋ฅผ ๊ณ ์ณ์ฃผ๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•œ๋‹ค.
if let popoverController = self.popoverController {
    popoverController.sourceRect = CGRect(
    x: size.width * 0.5,
    y: size.height * 0.5,
    width: 0,
    height: 0)
}
  • UIViewController extension์œผ๋กœ ๋งŒ๋“ค์–ด์ค€ ๋ฉ”์†Œ๋“œ ๋‚ด๋ถ€(๋งจ ์ฒ˜์Œ ์–ผ๋Ÿฟ์„ ์ƒ์„ฑํ•˜์—ฌ presentํ•˜๋Š” ๊ณณ)์—๋„ popoverController๋ฅผ ํ• ๋‹นํ•ด์ฃผ๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.
let splitViewController = self.splitViewController as? SplitViewController
splitViewController?.popoverController = popoverController

ํ•ด๊ฒฐ๋œ ๋ชจ์Šต

"UITableView Cell์„ selectRow๋ฅผ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•œ Crash"

UITableView์˜ selectRow๋ฅผ ํ†ตํ•ด Select๋ฅผ ์‹œ๋„ํ–ˆ์„ ๋•Œ, ์•„๋ž˜์™€ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด์„œ Crash๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

Thread 1: 
"Attempted to scroll the table view to an out-of-bounds row (0) when there are only 0 rows in section 0. 
Table view: <UITableView: 0x13f031400; 
frame = (0 0; 420 834); 
clipsToBounds = YES; 
autoresize = W+H; gestureRecognizers = <NSArray: 0x600000031680>;
layer = <CALayer: 0x600000ec7b80>; contentOffset: {0, -74}; 
contentSize: {420, 72.5}; adjustedContentInset: {74, 0, 20, 0}; 
dataSource: <CloudNotes.MemoListViewController: 0x14880fad0>>"
  • ์ƒํ™ฉ ๋ฉ”๋ชจ์žฅ์˜ ๋งˆ์ง€๋ง‰ ๋‚จ์€ ์…€์„ ์ง€์šฐ๊ฒŒ ๋˜๋ฉด์„œ selectRow๊ฐ€ ํ˜ธ์ถœ์ด ๋˜๋Š” ์ƒํ™ฉ์ด์˜€๋‹ค.
  • ์ด์œ  ์…€์„ ์ง€์šฐ๊ณ  ๋‚œ ํ›„๋‹ˆ๊นŒ UITableView์— ๋ณด์—ฌ์ค„ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๊ณ , Cell๋„ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ™ฉ์ด์˜€๋Š”๋ฐ, ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์…€์„ Select๋ฅผ ํ•˜๋ ค๊ณ  ํ•ด์„œ ํฌ๋ž˜์‰ฌ๊ฐ€ ๋‚œ ๊ฒƒ์ด์˜€๋‹ค.
  • ํ•ด๊ฒฐ ๋”ฐ๋ผ์„œ Select๋ฅผ ํ•˜๊ธฐ ์ „์— ๋จผ์ € UITableView์— numberOfRows(inSection:) ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ๊ฐ’์ด 0์ด ์•„๋‹ ๊ฒฝ์šฐ์—๋งŒ seletRow๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก guard๋ฌธ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ํ•ด๊ฒฐํ•ด์ฃผ์—ˆ๋‹ค.
func updateData(at index: Int) {
    guard self.tableView.numberOfRows(inSection: .zero) != .zero else {
        return
    }
    ...
    tableView.selectRow(at: IndexPath(row: .zero, section: .zero), animated: false, scrollPosition: .middle)
}

"UITableView์˜ Cell์„ deleteRows๋กœ ์ง€์› ์„ ๋•Œ ๋ฐœ์ƒํ•œ Crash"

JSON ๋ชจ๋ธ์—์„œ Core Data๋กœ ๋ฆฌํŒฉํ† ๋ง ๊ณผ์ •์—์„œ ๋‚œ ์—๋Ÿฌ์˜€๋‹ค.

Thread 1: 
"Invalid update: invalid number of rows in section 0. 
The number of rows contained in an existing section after the update (15) must be equal to the number of rows contained in that section before the update (15), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
 Table view: 
<UITableView: 0x11081d400;
 frame = (0 0; 420 1194); 
clipsToBounds = YES; 
autoresize = W+H; 
gestureRecognizers = <NSArray: 0x6000033708a0>; layer = <CALayer: 0x600003d8cb40>; 
contentOffset: {0, -74}; 
contentSize: {420, 1160}; 
adjustedContentInset: {74, 0, 20, 0};
 dataSource: <CloudNotes.NotesViewController: 0x12a808ee0>>"
  • ์ƒํ™ฉ ํ…Œ์ด๋ธ” ๋ทฐ์˜ ์„น์…˜์˜ ํ–‰ ๊ฐœ์ˆ˜์™€ ์‹ค์ œ ๋ณด์—ฌ์ค„ ์„น์…˜ ๊ฐœ์ˆ˜๊ฐ€ ๋งž์ง€ ์•Š์•„์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜์ด๋‹ค.
  • ์ด์œ  ํ…Œ์ด๋ธ”๋ทฐ์˜ ์…€์„ ์‚ญ์ œํ•˜๋ฉด์„œ ํ…Œ์ด๋ธ”๋ทฐ์— ๋ณด์—ฌ์ค„ ๋ฐ์ดํ„ฐ๋„ ๋™์ผํ•˜๊ฒŒ ์‚ญ์ œ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š”๋ฐ, ๋ˆ„๋ฝ๋˜์„œ ๋ฐœ์ƒํ•œ ๊ฒƒ์ด์˜€๋‹ค.
  • ํ•ด๊ฒฐ ์…€์„ ์ถ”๊ฐ€, ์‚ญ์ œํ•  ๋•Œ ํ…Œ์ด๋ธ”๋ทฐ์— ๋ณด์—ฌ์ค„ ์„น์…˜์˜ ๊ฐœ์ˆ˜๋„ ๋™์ผํ•  ์ˆ˜ ์žˆ๋„๋ก PersistentManager์˜ notes ๊ด€๋ฆฌ(๋ฐฐ์—ด ์š”์†Œ ์ œ๊ฑฐ, ์ฝ”์–ด๋ฐ์ดํ„ฐ ์š”์†Œ ์ œ๊ฑฐ)๋„ ๋นผ๋จน์ง€ ์•Š๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.
  • ๋Š๋‚€์  Cell์„ ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด selectํ•˜๊ฑฐ๋‚˜ insert, delete ๋“ฑ๊ณผ ๊ฐ™์€ ์ž‘์—…์„ ํ•  ๋•Œ์—๋Š” ์œ„์™€๊ฐ™์€ Crash๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ๋ฐ์ดํ„ฐ์™€ Cell์˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ผผ๊ผผํžˆ ํ•˜๋„๋ก ์œ ์˜ํ•ด์•ผ๊ฒ ๋‹ค๋Š” ์ƒ๊ฐ์ด ๋“ค์—ˆ๋‹ค.

"download๊ฐ€ ๋๋‚˜๋Š” ์‹œ์ ์— ๋ทฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ"

๋‹ค์šด๋กœ๋“œ๊ฐ€ ๋๋‚œ ํ›„ CoreData๋ฅผ fetch๋ฅผ ํ•˜๊ณ  TableView๋ฅผ reload๋ฅผ ํ•ด์ฃผ๊ณ  ์‹ถ์—ˆ์œผ๋‚˜ ์‹คํŒจํ–ˆ์—ˆ๋‹ค.

  • ์ด์œ  ํŒŒ์ผ์ด ์—ฌ๋Ÿฌ๊ฐœ๊ฐ€ ์กด์žฌํ•˜์—ฌ, ์—ฌ๋Ÿฌ๊ฐœ์˜ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œ ํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ˜๋ณต๋ฌธ์„ ๋Œ๋ฆฌ๊ณ  ์žˆ์—ˆ์œผ๋‚˜, fetch์™€ reload๋ฅผ for-in๋ฌธ ๋‚ด๋ถ€์—์„œ ํ•ด์ฃผ๊ณ  ์žˆ์–ด์„œ, ๋ทฐ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋  ๋•Œ๊ฐ€ ์žˆ๊ณ , ์•ˆ๋˜๊ธฐ๋„ ํ•˜๋Š” ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚ฌ๋‹ค.

* `ํ•ด๊ฒฐ` ๊ทธ๋ž˜์„œ `for-in๋ฌธ์ด ์ข…๋ฃŒ๋œ ์‹œ์ `์— `fetch`๋ฅผ ํ•˜๊ณ  view๋ฅผ reload๋ฅผ ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด, ๋‹ค์šด๋กœ๋“œ๊ฐ€ ๋ชจ๋‘ ์™„๋ฃŒ๋˜๋Š” ์‹œ์ ์„ `DispatchGroup`๋ฅผ ํ™œ์šฉํ•˜์—ฌ `์ถ”์ `ํ•˜๊ณ , ๋ฐ˜๋ณต๋ฌธ์—์„œ ์‹œ์ž‘๋˜์—ˆ๋˜ ๋‹ค์šด๋กœ๋“œ ์ž‘์—…์ด ๋ชจ๋‘ ๋๋‚˜๊ฒŒ ๋˜๋ฉด ์•„๋ž˜ ๋ทฐ๋ฅผ ๋‹ค์‹œ ์„ค์ •ํ•˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์˜€๋‹ค.
func download(_ tableViewController: NotesViewController?) {
    let group = DispatchGroup() // ๊ทธ๋ฃน ์ƒ์„ฑ
    for fileName in fileNames {
        let destURL = applicationSupportDirectoryURL.appendingPathComponent(fileName)
        let destination: (URL, HTTPURLResponse) -> URL = { _, _ in
            return destURL
        }
        group.enter() // ์ž‘์—… ์‹œ์ž‘
        client?.files.download(path: fileName, overwrite: true, destination: destination)
            .response { _, error in
                if let error = error {
                    print(error)
                }
                group.leave() // ์ž‘์—… ๋
            }
    }
    group.notify(queue: .main) { // ๋ชจ๋“  ์ž‘์—…์ด ๋๋‚œ๋‹ค๋ฉด ...
        PersistentManager.shared.setUpNotes()
        tableViewController?.tableView.reloadData()
        tableViewController?.stopActivityIndicator()
    }
}

๐Ÿค” ๊ณ ๋ฏผํ–ˆ๋˜ ์ 

"์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ UI๋ฅผ ๊ตฌํ˜„"

  • ์ตœ๊ทผ์— ์ž‘์„ฑ, ์ˆ˜์ •ํ•˜์˜€๋˜ ๋ฉ”๋ชจ๊ฐ€ ์ƒ๋‹จ์œผ๋กœ ์˜ฌ๋ผ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๋ฉ”๋ชจ ๋ฆฌ์ŠคํŠธ์˜ ์ •๋ ฌ์„ ๋‚ ์งœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ
  • ์–ด๋–ค ๋ฉ”๋ชจ๋ฅผ ์„ ํƒํ•ด์„œ ์ž‘์„ฑํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ํŽธํ•˜๋„๋ก ์ž‘์„ฑํ•˜๊ณ  ์žˆ๋Š” Cell์„ ๊ณ„์† Select ๋˜๋„๋ก ๊ตฌํ˜„
  • ์ž‘์„ฑํ•˜๋Š” ๋„์ค‘ ๋‚ ์งœ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜๋ฉด, ์ƒ๋‹จ์œผ๋กœ ์ด๋™ํ•˜๋ฉด์„œ Select๋„ ์ƒ๋‹จ์œผ๋กœ ์ด๋™.
  • ๋ฉ”๋ชจ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์„ ๋•Œ ์ถ”๊ฐ€ํ•œ ์ƒˆ๋กœ์šด ๋ฉ”๋ชจ๋ฅผ Select ๋˜๋„๋ก ๊ตฌํ˜„
  • ๋ฉ”๋ชจ๋ฅผ ์‚ญ์ œํ–ˆ์„ ๋•Œ, ์‚ญ์ œํ•œ ๋ถ€๋ถ„ ์ดํ›„ ๋ฉ”๋ชจ๋ฅผ ์ž๋™์œผ๋กœ Select ๋˜๋„๋ก ๊ตฌํ˜„
  • ์Šค์™€์ดํ”„ ๋ฐ ๋”๋ณด๊ธฐ ๋ฒ„ํŠผ ํ„ฐ์น˜์‹œ ๋ณด์—ฌ์ง€๋Š” ์•ก์…˜๋ฒ„ํŠผ์ด ๋‹จ์ˆœ ํ…์ŠคํŠธ๊ฐ€ ์•„๋‹Œ ์•„์ด์ฝ˜์ด ํ‘œ๊ธฐ๋˜๋„๋ก ๊ตฌํ˜„
  • Share๋ฅผ ํ„ฐ์น˜ํ•˜์—ฌ UIActivityViewController๊ฐ€ present ๋˜์—ˆ์„ ๋•Œ ํ™”๋ฉด ํšŒ์ „ ์‹œ์—๋„ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ค‘์•™์— ๊ณ„์† ์œ„์น˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„

"์ฝ”์–ด๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋งค๋‹ˆ์ € ํƒ€์ž… ๊ตฌํ˜„"

  • ๋ฉ”๋ชจ์˜ CRUD๋ฅผ ๊ตฌํ˜„ ๋ฐ View์— ๋ณด์—ฌ์ค„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” PersistentManager ๊ตฌํ˜„
  • fetch๋ฅผ ํ•  ๋•Œ Predicate, Sort ๋“ฑ์„ ์œ ์—ฐํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ„๋„ ๊ตฌํ˜„

"์ œ๋ชฉ๊ณผ ๋ณธ๋ฌธ์˜ ํฐํŠธ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ•˜์—ฌ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„"

  • AttributtedString์„ ์‚ฌ์šฉํ•˜์—ฌ TextView์˜ ์ œ๋ชฉ๊ณผ ๋ณธ๋ฌธ์˜ ํฐํŠธ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๊ธฐ์— ํŽธํ•˜๋„๋ก ๊ตฌํ˜„
  • textView์˜ delegate ๋ฉ”์„œ๋“œ(shouldChangeTextIn)์™€ textView์˜ typingAttributes ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž…๋ ฅ์ค‘์—๋„ ์ œ๋ชฉ๊ณผ ๋ณธ๋ฌธ์— ๋งž๋Š” ํฐํŠธ๊ฐ€ ์ ์šฉ๋˜๋„๋ก ๊ตฌํ˜„

"ํด๋ผ์šฐ๋“œ์— ์—…๋กœ๋“œํ•˜๋Š” ์‹œ์ "

  • ์—…๋กœ๋“œ ํ•˜๋Š” ์‹œ์ ์„ ์ •ํ•  ๋•Œ ๊ณ ๋ คํ•œ ๊ฒƒ์€ ๋„ˆ๋ฌด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์—…๋กœ๋“œ๋ฅผ ํ•˜์ง€ ์•Š์•„์•ผ ํ•˜๋ฉฐ, ์•ฑ์ด ์˜๋„์น˜ ์•Š๊ฒŒ ์ข…๋ฃŒ๋˜์–ด๋„ ์–ด๋Š์ •๋„ ๋ฐฉ์–ด๊ฐ€ ๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค.
  • ์ฒ˜์Œ์—๋Š” ํ…์ŠคํŠธ๋ทฐ์— ๋ณ€ํ™”๊ฐ€ ์ผ์–ด๋‚  ๋•Œ๋งˆ๋‹ค ํ•˜๋ ค๊ณ  ํ•˜์˜€์œผ๋‚˜ ๋„ˆ๋ฌด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚  ๊ฒƒ์œผ๋กœ ๋ณด์˜€๋‹ค. ๊ทธ๋ž˜์„œ ์ข…๋ฃŒํ•  ๋•Œ ํ•˜๋ ค๊ณ  ํ•˜๋‹ˆ ์˜๋„์น˜ ์•Š์€ ์ข…๋ฃŒ์— ์ „ํ˜€ ๋ฐฉ์–ด๊ฐ€ ๋˜์ง€ ์•Š์•˜๋‹ค.
  • ์—ฌ๋Ÿฌ ๊ณ ๋ฏผ์„ ํ•œ ๊ฒฐ๊ณผ ํ‚ค๋ณด๋“œ๋ฅผ ๋‚ด๋ฆด ๋•Œ ๋งˆ๋‹ค ์—…๋กœ๋“œ๋ฅผ ํ•˜๋„๋ก ํ•˜์—ฌ ์—…๋กœ๋“œ๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚˜์ง€ ์•Š๋„๋ก ํ•˜์˜€๊ณ  ์˜๋„์น˜ ์•Š๊ฒŒ ์ข…๋ฃŒ๋˜์–ด๋„ ๋ฐฉ์–ด๋ฅผ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.

"ํด๋ผ์šฐ๋“œ์— ๋‹ค์šด๋กœ๋“œํ•˜๋Š” ์‹œ์ "

  • ๋‹ค์šด๋กœ๋“œ ์‹œ์ ์€ ์ฒ˜์Œ์— ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ์„ฑ๊ณตํ•˜๋Š” ์‹œ์ ์— dropbox์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์šด๋กœ๋“œ ํ•˜์—ฌ ๋ณด์—ฌ์ฃผ๋Š” ์ฃผ๋„๋ก ๊ตฌํ˜„ํ•˜๊ธธ ์›ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ธ์ฆ์ด ์™„๋ฃŒ ๋˜๊ณ  authResult(์ธ์ฆ๊ฒฐ๊ณผ)๊ฐ€ success๊ฐ€ ๋˜๋ฉด download๋ฅผ ํ•˜๋„๋ก ํ•˜์˜€๋‹ค.
  • ๋˜ํ•œ, ๋‹ค์šด๋กœ๋“œ๋Š” ๋น„๋™๊ธฐ๋กœ ์ง„ํ–‰์ด ๋˜๊ธฐ ๋–„๋ฌธ์— DispatchGroup์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์•ฑ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฟŒ๋ ค์ฃผ๊ณ  ํ…Œ์ด๋ธ”๋ทฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

"๋‹ค์šด๋กœ๋“œ๊ฐ€ ์ง„ํ–‰์ค‘์ผ ๋•Œ ๋ทฐ์˜ ์ƒํƒœ"

  • ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์ง„ํ–‰๋  ๋™์•ˆ ๋ทฐ๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๊ฒŒ ๋œ๋‹ค.
  • ๋กœ๋”ฉ์ค‘์ด๋ผ๋Š” ๊ฒƒ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ ค์ฃผ๊ธฐ ์œ„ํ•ด์„œ ActivityIndicator๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค.
  • ๋‹ค์šด๋กœ๋“œ๋ฅผ ์š”์ฒญํ•˜๊ณ  ActivityIndicator๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ฃผ๋„๋ก ํ•˜๊ณ  ๋‹ค์šด๋กœ๋“œ๊ฐ€ ๋ชจ๋‘ ์™„๋ฃŒ๋˜๋ฉด ActivityIndicator๋Š” ์ข…๋ฃŒ๋˜๋ฉฐ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

๐Ÿ”ฅ ์ƒˆ๋กญ๊ฒŒ ์•Œ๊ฒŒ๋œ ๊ฒƒ

"UITableView reloadRows ๋ฅผ ํ™œ์šฉํ•ด ์ˆ˜์ •๋œ row๋งŒ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ"

tableView.reloadData๋กœ ํ…Œ์ด๋ธ”๋ทฐ์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค๋ฉด ๋„ˆ๋ฌด ๋น„ํšจ์œจ์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์—ฌ ์ˆ˜์ •๋œ ๋ถ€๋ถ„๋งŒ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

  1. MemoListViewController์—์„œ MemoDetailViewController๋กœ ํ™”๋ฉด์ „ํ™˜๋ ๋•Œ ํ„ฐ์น˜๋œ ํ…Œ์ด๋ธ”๋ทฐ์…€์˜ indexPath๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ํ”„๋กœํผํ‹ฐ๋กœ ์ €์žฅํ•œ๋‹ค.
  2. indexPath๋ฅผ SplitViewController๋กœ ์ „๋‹ฌํ•˜์—ฌ SplitViewController๊ฐ€ ํ”„๋กœํผํ‹ฐ๋กœ ๊ฐ€์ง€๊ณ ์žˆ๋Š” primaryVC์˜ updateData ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  3. MemoListViewController์—์„œ ์ „๋‹ฌ๋ฐ›์€ indexPath๋กœ ํ•ด๋‹น๋˜๋Š” ์…€์˜ ๋ฐ์ดํ„ฐ๋งŒ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.
extension MemoListViewController {
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let splitVC = self.splitViewController as? SplitViewController else {
            return
        }
        splitVC.present(at: indexPath.row)
    }
}
class SplitViewController: UISplitViewController {

    func present(at indexPath: Int) {
        let title = memoList[indexPath].title
        let body = memoList[indexPath].body
        secondaryVC.updateTextView(with: MemoDetailInfo(title: title, body: body))
        secondaryVC.updateIndex(with: indexPath)
        show(.secondary)
    }
}
extension MemoDetailViewController: UITextViewDelegate {
    private var currentIndex: Int = 0
    func textViewDidChange(_ textView: UITextView) {
        guard let splitVC = self.splitViewController as? SplitViewController else {
            return
        }
        let memo = createMemoData(with: textView.text)
        splitVC.updateMemoList(at: currentIndex, with: memo)
    }
}
class SplitViewController: UISplitViewController {
    func updateMemoList(at index: Int, with data: Memo) {
        memoList[index] = data
        let title = data.title.prefix(Constans.maximumTitleLength).description
        let body = data.body.prefix(Constans.maximumBodyLength).description
        let lastModified = data.lastModified.formattedDate
        let memoListInfo = MemoListInfo(title: title, body: body, lastModified: lastModified)
        primaryVC.updateData(at: index, with: memoListInfo)
    }
}
class MemoListViewController: UITableViewController {
    func updateData(at index: Int, with data: MemoListInfo) {
        memoListInfo[index] = data
        tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .none)
    }
}

"Codegen์ด๋ž€?"

https://developer.apple.com/documentation/coredata/modeling_data/generating_code

ํ”„๋กœ์ ํŠธ ํ•˜๋Š” ๋„์ค‘์— codegen์„ ์–ด๋–ค ์˜ต์…˜์œผ๋กœ ์ค˜์•ผํ• ์ง€ ๊ฐ์ด ์•ˆ์™€์„œ ์ฐพ์•„๋ณด์•˜๋‹ค.

  • ํ•ด๋‹น entity์— ๋Œ€ํ•œ ํด๋ž˜์Šค ์„ ์–ธ์„ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ์˜ต์…˜์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    • None/Manual: ๊ด€๋ จ ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์ง€ ์•Š๋Š”๋‹ค. ๊ฐœ๋ฐœ์ž๋Š” DataModel์„ ์„ ํƒํ•œ ์ƒํƒœ์—์„œ Editor-Create NSManagedObject Subclass ํ•ญ๋ชฉ์„ ํด๋ฆญํ•˜์—ฌ ํด๋ž˜์Šค ์„ ์–ธ ํŒŒ์ผ๊ณผ ํ”„๋กœํผํ‹ฐ extension ํŒŒ์ผ์„ ๋นŒ๋“œ์‹œ๋งˆ๋‹ค ์ถ”๊ฐ€์‹œ์ผœ ์ฃผ๊ณ , ์ด๋ฅผ ์ˆ˜๋™์œผ๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.
    • Class Definition: ํด๋ž˜์Šค ์„ ์–ธ ํŒŒ์ผ๊ณผ ํ”„๋กœํผํ‹ฐ ๊ด€๋ จ extension ํŒŒ์ผ์„ ๋นŒ๋“œ์‹œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ถ”๊ฐ€์‹œ์ผœ์ค€๋‹ค. ๋”ฐ๋ผ์„œ ๊ด€๋ จ๋œ ํŒŒ์ผ์„ ์ „ํ˜€ ์ถ”๊ฐ€์‹œ์ผœ์ค„ ํ•„์š”๊ฐ€ ์—†๋‹ค.(๊ทธ๋ž˜์„œ๋„ ์•ˆ๋œ๋‹ค. ๋งŒ์•ฝ ์ˆ˜๋™์œผ๋กœ ์ถ”๊ฐ€์‹œ์ผœ์ค€ ์ƒํƒœ์—์„œ ๋นŒ๋“œ๋ฅผ ์‹œ๋„ํ•˜๋ฉด ์ปดํŒŒ์ผ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.)
    • Category/Extension: ํ”„๋กœํผํ‹ฐ ๊ด€๋ จ extensionํŒŒ์ผ๋งŒ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€์‹œ์ผœ ์ค€๋‹ค. ์ฆ‰, ํด๋ž˜์Šค ์„ ์–ธ์—๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ๋กœ์ง์„ ์ž์œ ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.

"์ ‘๊ทผ์„ฑ์„ ์œ„ํ•ด Accessibility Inspector๋ฅผ ํ™œ์šฉ"

  • Accessibility Inspector๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ ‘๊ทผ์„ฑ์„ ์œ„ํ•ด Run Audit์„ ํ†ตํ•ด ๊ฐœ์„ ํ•  ํ•ญ๋ชฉ๋“ค์ด ์—†๋Š”์ง€ ๊ฒ€์ˆ˜ํ•˜์˜€๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  VoiceOver๋ฅผ ์ง์ ‘ ์‹คํ–‰ํ•ด์„œ ํ…Œ์ŠคํŠธํ•ด๋ณด๋ฉฐ ๋ถ€์กฑํ•œ ๋ถ€๋ถ„์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด์•˜๋‹ค.
  • ๋‹ค์ด๋‚˜๋ฏน ํƒ€์ž…์˜ ๊ฒฝ์šฐ๋„ ํ…์ŠคํŠธ ํฌ๊ธฐ๊ฐ€ ์œ ์—ฐํ•œ์ง€ ๊ฒ€์ˆ˜ํ•˜์˜€๋‹ค.

top

[ํ•™์Šต ๊ธฐ๋ก ํ”์ ]

๐Ÿ—‚ย ๋ชฉ์ฐจ

STEP 1 : ๋ฆฌ์ŠคํŠธ ๋ฐ ๋ฉ”๋ชจ์˜์—ญ ํ™”๋ฉด UI๊ตฌํ˜„

๋ฆฌ์ŠคํŠธ ํ™”๋ฉด๊ณผ ๋ฉ”๋ชจ์˜์—ญ ํ™”๋ฉด์„ SplitViewController๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

1-1 ๊ณ ๋ฏผํ–ˆ๋˜ ๊ฒƒ

1. ํ‚ค๋ณด๋“œ ๊ฐ€๋ฆผํ˜„์ƒ ๊ฐœ์„  ๋ฐ ํŽธ์ง‘๋ชจ๋“œ ์ข…๋ฃŒ ๊ตฌํ˜„

  • NotificationCenter๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ‚ค๋ณด๋“œ๊ฐ€ ํ™”๋ฉด์— ํ‘œ์‹œ๋  ๋•Œ UITextView๋„ ํ‚ค๋ณด๋“œ์˜ ๋†’์ด๋งŒํผ contentInset์„ ์กฐ์ •ํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.
  • ํŽธ์ง‘์„ ๋๋‚ธ ํ›„ ๋‹ค๋ฅธ ๋ฉ”๋ชจ๋ฅผ ๋ˆŒ๋ €์„ ๋•Œ ํŽธ์ง‘๋ชจ๋“œ๋ฅผ ์ข…๋ฃŒํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค. UITapGestureRecognizer๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ํ…์ŠคํŠธ๋ทฐ๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ๋ถ€๋ถ„์„ ํ„ฐ์น˜ํ–ˆ์„ ๋•Œ endEditing ๋ฉ”์†Œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋„๋ก ํ•˜์˜€๋‹ค.

2. ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ˆ˜์ •๋œ ๋ฉ”๋ชจ๊ฐ€ UITableView์— ๋ฐ˜์˜๋˜๋„๋ก ๊ตฌ์„ฑ

  • ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜์„ ์œ„ํ•ด UITextViewDelegate๋ฅผ ํ™œ์šฉํ•˜์—ฌ UITextView๊ฐ€ ์ˆ˜์ •๋  ๋•Œ ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ์ˆ˜์ •ํ•˜๊ณ , UITableView๋„ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

3. Crash๋ฅผ ๋ฐฉ์ง€

  • ์กด์žฌํ•˜์ง€์•Š๋Š” ์ธ๋ฑ์Šค๋ฅผ ์กฐํšŒํ–ˆ์„ ๋•Œ Crash๊ฐ€ ๋‚˜์ง€ ์•Š๋„๋ก subscript๋ฅผ ํ™œ์šฉํ•˜์—ฌ Crash๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.
extension Collection {
    subscript (safe index: Index) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

4. Dynamic Type

  • UILabel, UITextView์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ธ€์”จ ํฌ๊ธฐ๋ฅผ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋‹ค์ด๋‚˜๋ฏน ํฐํŠธ ์„ค์ • ๋ฐ Automatically Adjusts Font ๊ธฐ๋Šฅ์„ ํ™œ์„ฑํ™” ํ•ด์ฃผ์—ˆ๋‹ค.

5. ๋ฉ”๋ชจ๋ฅผ ํ„ฐ์น˜ํ–ˆ์„ ๋•Œ secondary ๋ทฐ์ปจ์— ์ƒ์„ธ ๋ฉ”๋ชจ๋ฅผ ํ‘œ์‹œํ•˜๋„๋ก ๊ตฌํ˜„

  • MemoListViewController์˜ UITableViewDelegate ๋ฉ”์„œ๋“œ didSelectRowAt์—์„œ SplitViewController์˜ present๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ๋ˆŒ๋ฆฐ ํ…Œ์ด๋ธ”๋ทฐ ์…€์˜ indexPath๋ฅผ ํ™œ์šฉํ•˜์˜€๋‹ค.
  • indexPath๋กœ SplitViewController๊ฐ€ ๊ฐ€์ง€๊ณ ์žˆ๋Š” Memo ๋ฐฐ์—ด ํƒ€์ž…์˜ ๋ฐ์ดํ„ฐ ์ค‘์—์„œ ํ•ด๋‹น๋˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๊ณจ๋ผ์„œ MemoDetailViewController์˜ text view๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.

1-2 ์˜๋ฌธ์ 

  • translatesAutoresizingMaskIntoConstraints๋Š” ์™œ false๋กœ ์ง€์ •ํ•ด์ฃผ๋Š” ๊ฑธ๊นŒ?
  • ํŠน์ • ํ–‰์— ํ•ด๋‹น๋˜๋Š” ์…€์„ ์—…๋ฐ์ดํŠธ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ์„๊นŒ?
  • GestureRecognizer๋ฅผ ๋“ฑ๋กํ–ˆ์„ ๋•Œ UITableViewDelagate๊ฐ€ ์™œ ๋จนํ†ต์ด์ง€?
  • ์‹คํ–‰ ์‹œ primaryVC์ด ๋ณด์—ฌ์กŒ์œผ๋ฉด ์ข‹๊ฒ ๋Š”๋ฐ...
  • SplitViewController์˜ secondaryVC์€ ์™œ ๋ฐฐ๊ฒฝ์ƒ‰์ด ํšŒ์ƒ‰์ด์ง€?
  • ๋ฐ์ดํ„ฐ๋ฅผ primary์™€ secondary์— ํšจ์œจ์ ์œผ๋กœ ๋ฟŒ๋ ค์ค„ ์ˆœ ์—†์„๊นŒ?

1-3 Trouble Shooting

1. Cell์˜ Select๊ฐ€ ๋จนํžˆ๋Š” ๋ฌธ์ œ

  • ์ƒํ™ฉ GestureRecognizer๋ฅผ ViewController์— ์ถ”๊ฐ€ํ•˜์ž UITableView์˜ Select๊ฐ€ ๋˜์ง€ ์•Š๋Š” ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚ฌ๋‹ค.
  • ์ด์œ  ๋“ฑ๋กํ•œ GestureRecognizer์˜ ํ”„๋กœํผํ‹ฐ์ธ cancelsTouchesInView๊ฐ€ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ true๋กœ ์„ค์ •๋˜์–ด์žˆ์–ด ๋ฌธ์ œ์˜€๋‹ค. cancelsTouchesInView๊ฐ€ true์ธ ๊ฒฝ์šฐ์—๋Š” ์ œ์Šค์ฒ˜๋ฅผ ์ธ์‹ํ•œ ํ›„์— ๋‚˜๋จธ์ง€ ํ„ฐ์น˜์ •๋ณด๋“ค์„ ๋ทฐ๋กœ ์ „๋‹ฌํ•˜์ง€ ์•Š๊ณ  ์ทจ์†Œ๋˜์—ˆ๊ธฐ ๋•Œ๋ฌธ์— UITableView์˜ Select๊ฐ€ ๋จน์ง€ ์•Š์•˜๋˜ ๊ฒƒ์ด๋‹ค.
  • ํ•ด๊ฒฐ ๋”ฐ๋ผ์„œ cancelsTouchesInView๊ฐ’์„ false๋กœ ํ• ๋‹นํ•ด์คŒ์œผ๋กœ์จ ํ•ด๋‹น ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜์˜€๋‹ค. ์ œ์Šค์ฒ˜๋ฅผ ์ธ์‹ํ•œ ํ›„์—๋„ Gesture Recognizer์˜ ํŒจํ„ด๊ณผ๋Š” ๋ฌด๊ด€ํ•˜๊ฒŒ ํ„ฐ์น˜ ์ •๋ณด๋“ค์„ ๋ทฐ์— ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค.

2. ๋ฉ”๋ชจ์žฅ์— ํ…์ŠคํŠธ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ Crash๋‚˜๋Š” ๋ฌธ์ œ

  • ์ƒํ™ฉ ๋ฉ”๋ชจ์žฅ์— linebreak๊ฐ€ 1๊ฐœ์ผ ๋•Œ Crash๊ฐ€ ๋‚˜๋Š” ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚ฌ๋‹ค. ์•„๋ž˜๋Š” ๋ชจ๋“  ๋ฉ”๋ชจ๋ฅผ ์ง€์› ์„ ๊ฒฝ์šฐ Crash๊ฐ€ ๋‚˜๋Š” ์ƒํ™ฉ์ด๋‹ค.
  • ์ด์œ  ๋ฐฐ์—ด์„ ์กฐํšŒํ•  ๋•Œ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ธ๋ฑ์Šค๋ฅผ ์กฐํšŒํ•  ๊ฒฝ์šฐ ์•ฑ์ด ์ฃฝ์–ด๋ฒ„๋ฆฌ๋Š” ์ƒํ™ฉ์ด์˜€๋˜ ๊ฒƒ์ด๋‹ค.
  • ํ•ด๊ฒฐ ๋”ฐ๋ผ์„œ ์ธ๋ฑ์Šค๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ์กฐํšŒํ•˜๋„๋ก subscript๋ฅผ extension ํ•ด์ฃผ์–ด ์กฐํšŒ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๋Œ€์ฒ˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ด๊ฒฐํ•˜์˜€๋‹ค.
    extension Collection {
        subscript (safe index: Index) -> Element? {
            return indices.contains(index) ? self[index] : nil
        }
    }

1-4 ๋ฐฐ์šด ๊ฐœ๋…

Split View์—์„œ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ถ•์†Œ๋˜์—ˆ์„๋•Œ ๋จผ์ € ๋ณด์—ฌ์ง€๋Š” ๋ทฐ๋ฅผ secondary๊ฐ€ ์•„๋‹ˆ๋ผ primary๋กœ ์„ค์ •ํ•˜๊ธฐ

Split View์—์„œ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ถ•์†Œ๋˜์—ˆ์„๋•Œ ๋จผ์ € ๋ณด์—ฌ์ง€๋Š” ๋ทฐ๋ฅผ secondary๊ฐ€ ์•„๋‹ˆ๋ผ primary๋กœ ์„ค์ •ํ•˜๊ธฐ

  • ์•„์ดํŒจ๋“œ์—์„œ ์Šคํ”Œ๋ฆฟ๋ทฐ๋กœ ๋‹ค๋ฅธ ์•ฑ๊ณผ ํ™”๋ฉด์„ ๊ฐ™์ด ์“ฐ๋Š” ๊ฒฝ์šฐ ํ™”๋ฉด์ด ์ž‘์•„์ ธ์„œ primary์™€ secondary๋ทฐ๊ฐ€ ํ•œ๋ฒˆ์— ๋ณด์ด์ง€ ์•Š์•˜๋‹ค. primary๋ทฐ์ธ ๋ฉ”๋ชจ๋ชฉ๋ก์ด ๋จผ์ € ๋ณด์—ฌ์ง€๊ฒŒ ํ•˜๊ณ  ์‹ถ์—ˆ๋Š”๋ฐ secondary๋ทฐ์ธ ๋ฉ”๋ชจ์žฅ์ด ๋จผ์ € ๋ณด์—ฌ์ง€๋Š” ํ˜„์ƒ์ด ๋ฐœ์ƒํ•˜์˜€๋‹ค.
  • ๋””ํดํŠธ ๊ฐ’์ด secondary๋ทฐ์ž„์„ ํ™•์ธํ•˜๊ณ  primary๊ฐ€ ๋จผ์ € ๋ณด์—ฌ์ง€๋„๋ก delegate ๋ฉ”์„œ๋“œ๋ฅผ ํ†ตํ•ด ์„ค์ •ํ•ด์ฃผ์—ˆ๋‹ค.
extension SplitViewController: UISplitViewControllerDelegate {
    func splitViewController(
        _ svc: UISplitViewController,
        topColumnForCollapsingToProposedTopColumn proposedTopColumn: UISplitViewController.Column
    ) -> UISplitViewController.Column {
        return .primary
    }
}
DateFormatter ์ง€์—ญํ™”

DateFormatter ์ง€์—ญํ™”

  • TimeInterval ํƒ€์ž…์œผ๋กœ ์ฃผ์–ด์ง„ ๋ฉ”๋ชจ ์ž‘์„ฑ๋‚ ์งœ๋ฅผ ๋‚ ์งœ ํ˜•์‹์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ์œ„ํ•ด TimeInterval ํƒ€์ž…์„ extensionํ•˜์—ฌ ์—ฐ์‚ฐ ํ”„๋กœํผํ‹ฐ๋ฅผ ๊ตฌํ˜„ํ•˜์˜€๋‹ค.
  • ์‚ฌ์šฉ์ž์˜ ์ง€์—ญ์— ๋งž๋Š” ๋‚ ์งœ๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด DateFormatter์˜ locale๋ฅผ ํ™œ์šฉํ•˜์˜€๋‹ค.
extension TimeInterval {
    var formattedDate: String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .short
        let localeID = Locale.preferredLanguages.first
        let deviceLocale = Locale(identifier: localeID ?? "ko-kr").languageCode
        dateFormatter.locale = Locale(identifier: deviceLocale ?? "ko-kr")
        dateFormatter.timeZone = TimeZone.current
        return dateFormatter.string(from: Date(timeIntervalSince1970: self))
    }
}
์ฝ”๋“œ๋กœ ๋ทฐ ๊ตฌํ˜„ํ•˜๊ธฐ: SceneDelegate ์—์„œ initial View Controller ์„ค์ •

์ฝ”๋“œ๋กœ ๋ทฐ ๊ตฌํ˜„ํ•˜๊ธฐ: SceneDelegate ์—์„œ initial View Controller ์„ค์ •

  • ์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ์ง€์šด ํ›„ SceneDelegate์˜ scene๋ฉ”์„œ๋“œ์—์„œ window์˜ rootViewController๋ฅผ ์•ฑ์˜ ์ฒซํ™”๋ฉด์— ๋ณด์ด๋Š” splitVC๋กœ ์„ค์ •ํ•œ๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  makeKeyAndVisible()๋กœ ํ™”๋ฉด์— ๋ณด์ด๋„๋ก ์„ค์ •ํ•˜์—ฌ Storyboard์—์„œ initial view controller๋กœ ์ง€์ •ํ•˜๋Š” ๊ฒƒ์„ ๋Œ€์‹ ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windoewScene = (scene as? UIWindowScene) else {
            return
        }
        window = UIWindow(windowScene: windoewScene)
        let splitVC = SplitViewController(style: .doubleColumn)
        window?.rootViewController = splitVC
        window?.makeKeyAndVisible()
    }
BarButtonItem ํ™œ์šฉ

BarButtonItem ํ™œ์šฉ

  • UIViewController์— ์žˆ๋Š” navigationItem ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ title๊ณผ BarButtonItem ๋“ฑ navigation์— ํ•„์š”ํ•œ item์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • UIBarButtonItem์˜ ์ด๋‹ˆ์…œ๋ผ์ด์ €์—๋Š” image๋ฅผ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›๊ฑฐ๋‚˜, barButtonSystemItem์„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฐ›์„ ์ˆ˜ ์žˆ์–ด ํ•„์š”ํ•œ ๊ฒƒ์„ ๊ณจ๋ผ์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
navigationItem.title = "๋ฉ”๋ชจ"
navigationItem.rightBarButtonItem = UIBarButtonItem(
    barButtonSystemItem: .add, 
    target: self, 
    action: nil
)
navigationItem.rightBarButtonItem = UIBarButtonItem(
    image: UIImage(systemName: "ellipsis.circle"),
    style: .plain,
    target: self,
    action: nil
)
UISplitViewController

UISplitViewController

  • setViewController(_:for:) : UISplitViewController์˜ ๋ฉ”์„œ๋“œ๋กœ Double Column ์Šคํƒ€์ผ์ธ ๊ฒฝ์šฐ์— primary์™€ secondary ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ์ง€์ •ํ•œ๋‹ค.
  • ์ด ๋ฉ”์„œ๋“œ๋กœ ์ง€์ •ํ•˜๋Š” ๊ฒฝ์šฐ์— ์ž๋™์œผ๋กœ ๋ทฐ์ปจํŠธ๋กค๋Ÿฌ์— ๋„ค๋น„๊ฒŒ์ด์…˜ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๊ฐ์‹ธ์„œ UISplitViewController์— ํ• ๋‹นํ•ด์ค€๋‹ค.
class SplitViewController: UISplitViewController {
    private let primaryVC = MemoListViewController(style: .insetGrouped)
    private let secondaryVC = MemoDetailViewController()
    
    private func setUpChildView() {
        setViewController(primaryVC, for: .primary)
        setViewController(secondaryVC, for: .secondary)
    }
}
UITableView reloadRows ๋ฅผ ํ™œ์šฉํ•ด ์ˆ˜์ •๋œ row๋งŒ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ

UITableView reloadRows ๋ฅผ ํ™œ์šฉํ•ด ์ˆ˜์ •๋œ row๋งŒ ์—…๋ฐ์ดํŠธํ•˜๊ธฐ

tableView.reloadData๋กœ ํ…Œ์ด๋ธ”๋ทฐ์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ์—…๋ฐ์ดํŠธํ•œ๋‹ค๋ฉด ๋„ˆ๋ฌด ๋น„ํšจ์œจ์ ์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜์—ฌ ์ˆ˜์ •๋œ ๋ถ€๋ถ„๋งŒ ์—…๋ฐ์ดํŠธํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

  1. MemoListViewController์—์„œ MemoDetailViewController๋กœ ํ™”๋ฉด์ „ํ™˜๋ ๋•Œ ํ„ฐ์น˜๋œ ํ…Œ์ด๋ธ”๋ทฐ์…€์˜ indexPath๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ํ”„๋กœํผํ‹ฐ๋กœ ์ €์žฅํ•œ๋‹ค.
  2. indexPath๋ฅผ SplitViewController๋กœ ์ „๋‹ฌํ•˜์—ฌ SplitViewController๊ฐ€ ํ”„๋กœํผํ‹ฐ๋กœ ๊ฐ€์ง€๊ณ ์žˆ๋Š” primaryVC์˜ updateData ๋ฉ”์„œ๋“œ๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
  3. MemoListViewController์—์„œ ์ „๋‹ฌ๋ฐ›์€ indexPath๋กœ ํ•ด๋‹น๋˜๋Š” ์…€์˜ ๋ฐ์ดํ„ฐ๋งŒ ์—…๋ฐ์ดํŠธํ•œ๋‹ค.
extension MemoListViewController {
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let splitVC = self.splitViewController as? SplitViewController else {
            return
        }
        splitVC.present(at: indexPath.row)
    }
}
class SplitViewController: UISplitViewController {

    func present(at indexPath: Int) {
        let title = memoList[indexPath].title
        let body = memoList[indexPath].body
        secondaryVC.updateTextView(with: MemoDetailInfo(title: title, body: body))
        secondaryVC.updateIndex(with: indexPath)
        show(.secondary)
    }
}
extension MemoDetailViewController: UITextViewDelegate {
    private var currentIndex: Int = 0
    func textViewDidChange(_ textView: UITextView) {
        guard let splitVC = self.splitViewController as? SplitViewController else {
            return
        }
        let memo = createMemoData(with: textView.text)
        splitVC.updateMemoList(at: currentIndex, with: memo)
    }
}
class SplitViewController: UISplitViewController {
    func updateMemoList(at index: Int, with data: Memo) {
        memoList[index] = data
        let title = data.title.prefix(Constans.maximumTitleLength).description
        let body = data.body.prefix(Constans.maximumBodyLength).description
        let lastModified = data.lastModified.formattedDate
        let memoListInfo = MemoListInfo(title: title, body: body, lastModified: lastModified)
        primaryVC.updateData(at: index, with: memoListInfo)
    }
}
class MemoListViewController: UITableViewController {
    func updateData(at index: Int, with data: MemoListInfo) {
        memoListInfo[index] = data
        tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .none)
    }
}

1-5 PR ํ›„ ๊ฐœ์„ ์‚ฌํ•ญ

  • View์˜ ๋ณด์—ฌ์ค„ ์š”์†Œ๋“ค์„ ๋ณ„๋„์˜ ํƒ€์ž…์œผ๋กœ ๋งŒ๋“ค์–ด ๋ณด์—ฌ์ฃผ์—ˆ๋˜ ๋ถ€๋ถ„์„ ์ œ๊ฑฐํ›„ Core Data๋กœ ๋ชจ๋‘ ํ†ต์ผํ•˜์—ฌ ๋ฆฌํŒฉํ† ๋ง

top

STEP 2 : ์ฝ”์–ด๋ฐ์ดํ„ฐ DB ๊ตฌํ˜„

๋ฉ”๋ชจ๋ฅผ ์œ„ํ•œ ์ฝ”์–ด๋ฐ์ดํ„ฐ ๋ชจ๋ธ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

2-1 ๊ณ ๋ฏผํ–ˆ๋˜ ๊ฒƒ

1. ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ UI๋ฅผ ๊ตฌํ˜„

  • ์ตœ๊ทผ์— ์ž‘์„ฑ, ์ˆ˜์ •ํ•˜์˜€๋˜ ๋ฉ”๋ชจ๊ฐ€ ์ƒ๋‹จ์œผ๋กœ ์˜ฌ๋ผ์˜ฌ ์ˆ˜ ์žˆ๋„๋ก ๋ฉ”๋ชจ ๋ฆฌ์ŠคํŠธ์˜ ์ •๋ ฌ์„ ๋‚ ์งœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ
  • ์–ด๋–ค ๋ฉ”๋ชจ๋ฅผ ์„ ํƒํ•ด์„œ ์ž‘์„ฑํ•˜๊ณ  ์žˆ๋Š”์ง€ ํ•œ๋ˆˆ์— ๋ณด๊ธฐ ํŽธํ•˜๋„๋ก ์ž‘์„ฑํ•˜๊ณ  ์žˆ๋Š” Cell์„ ๊ณ„์† Select ๋˜๋„๋ก ๊ตฌํ˜„
  • ์ž‘์„ฑํ•˜๋Š” ๋„์ค‘ ๋‚ ์งœ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋˜๋ฉด, ์ƒ๋‹จ์œผ๋กœ ์ด๋™ํ•˜๋ฉด์„œ Select๋„ ์ƒ๋‹จ์œผ๋กœ ์ด๋™.
  • ๋ฉ”๋ชจ๋ฅผ ์ถ”๊ฐ€ํ–ˆ์„ ๋•Œ ์ถ”๊ฐ€ํ•œ ์ƒˆ๋กœ์šด ๋ฉ”๋ชจ๋ฅผ Select ๋˜๋„๋ก ๊ตฌํ˜„
  • ๋ฉ”๋ชจ๋ฅผ ์‚ญ์ œํ–ˆ์„ ๋•Œ, ์‚ญ์ œํ•œ ๋ถ€๋ถ„ ์ดํ›„ ๋ฉ”๋ชจ๋ฅผ ์ž๋™์œผ๋กœ Select ๋˜๋„๋ก ๊ตฌํ˜„
  • ์Šค์™€์ดํ”„ ๋ฐ ๋”๋ณด๊ธฐ ๋ฒ„ํŠผ ํ„ฐ์น˜์‹œ ๋ณด์—ฌ์ง€๋Š” ์•ก์…˜๋ฒ„ํŠผ์ด ๋‹จ์ˆœ ํ…์ŠคํŠธ๊ฐ€ ์•„๋‹Œ ์•„์ด์ฝ˜์ด ํ‘œ๊ธฐ๋˜๋„๋ก ๊ตฌํ˜„
  • Share๋ฅผ ํ„ฐ์น˜ํ•˜์—ฌ UIActivityViewController๊ฐ€ present ๋˜์—ˆ์„ ๋•Œ ํ™”๋ฉด ํšŒ์ „ ์‹œ์—๋„ ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ์ค‘์•™์— ๊ณ„์† ์œ„์น˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„

2. ์ฝ”์–ด๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋งค๋‹ˆ์ € ํƒ€์ž… ๊ตฌํ˜„

  • ๋ฉ”๋ชจ์˜ CRUD๋ฅผ ๊ตฌํ˜„ ๋ฐ View์— ๋ณด์—ฌ์ค„ ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” PersistentManager ๊ตฌํ˜„
  • fetch๋ฅผ ํ•  ๋•Œ Predicate, Sort ๋“ฑ์„ ์œ ์—ฐํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํŒŒ๋ผ๋ฏธํ„ฐ ๋ณ„๋„ ๊ตฌํ˜„

3. ์ œ๋ชฉ๊ณผ ๋ณธ๋ฌธ์˜ ํฐํŠธ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ•˜์—ฌ ๊ตฌ๋ถ„ํ•˜๋Š” ๊ธฐ๋Šฅ ๊ตฌํ˜„

  • AttributtedString์„ ์‚ฌ์šฉํ•˜์—ฌ TextView์˜ ์ œ๋ชฉ๊ณผ ๋ณธ๋ฌธ์˜ ํฐํŠธ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋ณด๊ธฐ์— ํŽธํ•˜๋„๋ก ๊ตฌํ˜„
  • textView์˜ delegate ๋ฉ”์„œ๋“œ(shouldChangeTextIn)์™€ textView์˜ typingAttributes ํ”„๋กœํผํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž…๋ ฅ์ค‘์—๋„ ์ œ๋ชฉ๊ณผ ๋ณธ๋ฌธ์— ๋งž๋Š” ํฐํŠธ๊ฐ€ ์ ์šฉ๋˜๋„๋ก ๊ตฌํ˜„

2-2 ์˜๋ฌธ์ 

  • Core Data - codegen์„ ์–ด๋–ป๊ฒŒ ์„ค์ •ํ•ด์•ผ ์ ์ ˆํ• ๊นŒ?
  • NSFetchRequest - returnsObjectsAsFaults ์†์„ฑ๊ฐ’์€ ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š” ๊ฒƒ์ผ๊นŒ?
  • NSFetchRequestResult ํ”„๋กœํ† ์ฝœ์ด ๋ญ˜๊นŒ?
  • ๋ณธ๋ฌธ์˜ ์ œ๋ชฉ์„ ๊ตต๊ฒŒ ํ‘œ์‹œํ•˜๋ฉด์„œ ๋‹ค์ด๋‚˜๋ฏน ํƒ€์ž…์„ ์ ์šฉํ•  ์ˆ˜ ์žˆ์„๊นŒ?
  • UIContextualAction์˜ handler์˜ completeHandeler๋Š” ์–ด๋–ค ์—ญํ• ์„ ํ•˜๋Š” ๊ฒƒ์ผ๊นŒ?
  • UIAlertController - ActionSheet๋ฅผ iPad์—์„œ present ํ•˜๋ ค๋ฉด ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ•˜์ง€?

2-3 Trouble Shooting

1. iPad์—์„œ UIAlertController์˜ actionSheet ์‚ฌ์šฉ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜

์˜ค๋ฅ˜๋ฉ”์„ธ์ง€

  • UIActivityViewController๋ฅผ present๋ฅผ ํ•ด์ฃผ๋ ค๋Š”๋ฐ ์•„๋ž˜์™€ ๊ฐ™์€ ์˜ค๋ฅ˜๋ฉ”์„ธ์ง€๊ฐ€ ๋–ด๋‹ค.
Thread 1: "Your application has presented a UIAlertController (<UIAlertController: 0x10d813a00>) of style UIAlertControllerStyleActionSheet from CloudNotes.SplitViewController (<CloudNotes.SplitViewController: 0x11f7068f0>).
The modalPresentationStyle of a UIAlertController with this style is UIModalPresentationPopover. 
You must provide location information for this popover through the alert controller's popoverPresentationController.
You must provide either a sourceView and sourceRect or a barButtonItem. 
If this information is not known when you present the alert controller, you may provide it in the UIPopoverPresentationControllerDelegate method -prepareForPopoverPresentation."
  • ๊ฐ„๋‹จํžˆ ํ•ด์„ํ•˜์ž๋ฉด iPad์—์„œ ์•ก์…˜์‹œํŠธ๋ฅผ present๋ฅผ ํ•  ๊ฒฝ์šฐ ๋ชจ๋‹ฌ์Šคํƒ€์ผ์ด UIModalPresentationPopover์ด๊ณ , ์ด๊ฑธ ์‚ฌ์šฉํ•  ๋•Œ๋Š” barButtonItem ๋˜๋Š” ํ•ด๋‹น ์ฐฝ์˜ ๋Œ€ํ•œ ์œ„์น˜๋ฅผ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค๊ณ  ๋˜์–ด์žˆ๋‹ค.
  • ๋”ฐ๋ผ์„œ ์„ค์ •ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š” ๊ฒƒ์€ 2๊ฐ€์ง€์ค‘ ํ•˜๋‚˜์ด๋‹ค.
    • ํ•„์ˆ˜์ ์œผ๋กœ sourceView ์ง€์ •ํ•ด์ฃผ๊ธฐ
    • popoverPresentationController์— sourceRect ๋˜๋Š” barButtonItem ํ• ๋‹นํ•ด์ฃผ๊ธฐ

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

์–ผ๋Ÿฟ์„ present ํ•ด์ฃผ๊ธฐ ์ „์— ๋‹ค์Œ๊ณผ ๊ฐ™์€ if๋ฌธ์„ ์ถ”๊ฐ€ํ•ด์ฃผ์ž!

  • UIBarButtonItem์— ์ถ”๊ฐ€ํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•
// UIViewController extension ๋‚ด๋ถ€...
if let popoverController = activityViewController.popoverPresentationController {
    popoverController.sourceView = self.splitViewController?.view
    popoverController.barButtonItem = sender // ๋ฉ”์†Œ๋“œ ๋‚ด๋ถ€๋ผ์„œ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ barButtonItem ์ „๋‹ฌ๋ฐ›์•„ ํ• ๋‹นํ•ด์ฃผ์—ˆ๋‹ค.
}
  • ์œ„์น˜๋ฅผ ์ •ํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•
if let popoverController = activityViewController.popoverPresentationController {
    popoverController.sourceView = self.splitViewController?.view // presentํ•  ๋ทฐ ์ง€์ •
    popoverController.sourceRect = CGRect( // ๋ทฐ์˜ ์ • ๊ฐ€์šด๋ฐ ์œ„์น˜๋กœ ์ง€์ •
        x: self.splitViewController?.view.bounds.midX,
        y: self.splitViewController?.view.bounds.midY,
        width: 0,
        height: 0
    )
    popoverController.permittedArrowDirections = [] // ํ™”์‚ดํ‘œ๋ฅผ ๋นˆ๋ฐฐ์—ด๋กœ ๋Œ€์ž…
}

์˜๋ฌธ์ 

์œ„์น˜๋ฅผ ์ง€์ •ํ•˜๊ณ  ๋‚˜์„œ ํ™”๋ฉด์„ ๋Œ๋ ธ๋Š”๋ฐ... ๊ฐ€์šด๋ฐ ์œ„์น˜์— ์•ˆ์žˆ๊ณ  ์š”์ƒํ•œ ๊ณณ์— ์žˆ๋‹ค....

  • ํ•ด๊ฒฐ๋ฐฉ๋ฒ•
  • ํ™”๋ฉด์ด ๋Œ์•„๊ฐˆ ๋•Œ๋งˆ๋‹ค ํฌ์ง€์…˜์„ ๋‹ค์‹œ ์žก์•„์ฃผ๋ฉด ๋œ๋‹ค. ๊ทธ๊ฑธ ์œ„ํ•ด viewWillTransition ๋ฉ”์†Œ๋“œ๋ฅผ ํ™œ์šฉํ•ด๋ณด๊ฒ ๋‹ค.
    • ์ด ๋ฉ”์†Œ๋“œ๋Š” ViewController์˜ ๋ทฐ ํฌ๊ธฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ธฐ ์ „์— ํ˜ธ์ถœ์ด ๋œ๋‹ค.
  • ์ผ๋‹จ ์–ผ๋Ÿฟ์„ presentํ•˜๋Š” ๋ทฐ์— popoverController๋ผ๋Š” ๋ณ€์ˆ˜๋ฅผ ๋งŒ๋“ค์–ด์ค€๋‹ค.
class SplitViewController: UISplitViewController {
    ...
    var popoverController: UIPopoverPresentationController?
  • ๊ทธ๋ฆฌ๊ณ  viewWillTransition ๋ฉ”์†Œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜์—ฌ ์œ„์น˜๋ฅผ ๊ณ ์ณ์ฃผ๋Š” ๋กœ์ง์„ ์ถ”๊ฐ€ํ•œ๋‹ค.
if let popoverController = self.popoverController {
    popoverController.sourceRect = CGRect(
    x: size.width * 0.5,
    y: size.height * 0.5,
    width: 0,
    height: 0)
}
  • UIViewController extension์œผ๋กœ ๋งŒ๋“ค์–ด์ค€ ๋ฉ”์†Œ๋“œ ๋‚ด๋ถ€(๋งจ ์ฒ˜์Œ ์–ผ๋Ÿฟ์„ ์ƒ์„ฑํ•˜์—ฌ presentํ•˜๋Š” ๊ณณ)์—๋„ popoverController๋ฅผ ํ• ๋‹นํ•ด์ฃผ๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.
let splitViewController = self.splitViewController as? SplitViewController
splitViewController?.popoverController = popoverController

ํ•ด๊ฒฐ๋œ ๋ชจ์Šต

2. UITableView Cell์„ selectRow๋ฅผ ํ˜ธ์ถœํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•œ Crash

UITableView์˜ selectRow๋ฅผ ํ†ตํ•ด Select๋ฅผ ์‹œ๋„ํ–ˆ์„ ๋•Œ, ์•„๋ž˜์™€ ๊ฐ™์€ ์—๋Ÿฌ๊ฐ€ ๋‚˜๋ฉด์„œ Crash๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

Thread 1: 
"Attempted to scroll the table view to an out-of-bounds row (0) when there are only 0 rows in section 0. 
Table view: <UITableView: 0x13f031400; 
frame = (0 0; 420 834); 
clipsToBounds = YES; 
autoresize = W+H; gestureRecognizers = <NSArray: 0x600000031680>;
layer = <CALayer: 0x600000ec7b80>; contentOffset: {0, -74}; 
contentSize: {420, 72.5}; adjustedContentInset: {74, 0, 20, 0}; 
dataSource: <CloudNotes.MemoListViewController: 0x14880fad0>>"
  • ์ƒํ™ฉ ๋ฉ”๋ชจ์žฅ์˜ ๋งˆ์ง€๋ง‰ ๋‚จ์€ ์…€์„ ์ง€์šฐ๊ฒŒ ๋˜๋ฉด์„œ selectRow๊ฐ€ ํ˜ธ์ถœ์ด ๋˜๋Š” ์ƒํ™ฉ์ด์˜€๋‹ค.
  • ์ด์œ  ์…€์„ ์ง€์šฐ๊ณ  ๋‚œ ํ›„๋‹ˆ๊นŒ UITableView์— ๋ณด์—ฌ์ค„ ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๊ณ , Cell๋„ ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์ƒํ™ฉ์ด์˜€๋Š”๋ฐ, ์กด์žฌํ•˜์ง€ ์•Š๋Š” ์…€์„ Select๋ฅผ ํ•˜๋ ค๊ณ  ํ•ด์„œ ํฌ๋ž˜์‰ฌ๊ฐ€ ๋‚œ ๊ฒƒ์ด์˜€๋‹ค.
  • ํ•ด๊ฒฐ ๋”ฐ๋ผ์„œ Select๋ฅผ ํ•˜๊ธฐ ์ „์— ๋จผ์ € UITableView์— numberOfRows(inSection:) ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ํ•ด๋‹น ๊ฐ’์ด 0์ด ์•„๋‹ ๊ฒฝ์šฐ์—๋งŒ seletRow๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก guard๋ฌธ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ํ•ด๊ฒฐํ•ด์ฃผ์—ˆ๋‹ค.
func updateData(at index: Int) {
    guard self.tableView.numberOfRows(inSection: .zero) != .zero else {
        return
    }
    ...
    tableView.selectRow(at: IndexPath(row: .zero, section: .zero), animated: false, scrollPosition: .middle)
}

3. UITableView์˜ Cell์„ deleteRows๋กœ ์ง€์› ์„ ๋•Œ ๋ฐœ์ƒํ•œ Crash

JSON ๋ชจ๋ธ์—์„œ Core Data๋กœ ๋ฆฌํŒฉํ† ๋ง ๊ณผ์ •์—์„œ ๋‚œ ์—๋Ÿฌ์˜€๋‹ค.

Thread 1: 
"Invalid update: invalid number of rows in section 0. 
The number of rows contained in an existing section after the update (15) must be equal to the number of rows contained in that section before the update (15), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
 Table view: 
<UITableView: 0x11081d400;
 frame = (0 0; 420 1194); 
clipsToBounds = YES; 
autoresize = W+H; 
gestureRecognizers = <NSArray: 0x6000033708a0>; layer = <CALayer: 0x600003d8cb40>; 
contentOffset: {0, -74}; 
contentSize: {420, 1160}; 
adjustedContentInset: {74, 0, 20, 0};
 dataSource: <CloudNotes.NotesViewController: 0x12a808ee0>>"
  • ์ƒํ™ฉ ํ…Œ์ด๋ธ” ๋ทฐ์˜ ์„น์…˜์˜ ํ–‰ ๊ฐœ์ˆ˜์™€ ์‹ค์ œ ๋ณด์—ฌ์ค„ ์„น์…˜ ๊ฐœ์ˆ˜๊ฐ€ ๋งž์ง€ ์•Š์•„์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜์ด๋‹ค.
  • ์ด์œ  ํ…Œ์ด๋ธ”๋ทฐ์˜ ์…€์„ ์‚ญ์ œํ•˜๋ฉด์„œ ํ…Œ์ด๋ธ”๋ทฐ์— ๋ณด์—ฌ์ค„ ๋ฐ์ดํ„ฐ๋„ ๋™์ผํ•˜๊ฒŒ ์‚ญ์ œ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š”๋ฐ, ๋ˆ„๋ฝ๋˜์„œ ๋ฐœ์ƒํ•œ ๊ฒƒ์ด์˜€๋‹ค.
  • ํ•ด๊ฒฐ ์…€์„ ์ถ”๊ฐ€, ์‚ญ์ œํ•  ๋•Œ ํ…Œ์ด๋ธ”๋ทฐ์— ๋ณด์—ฌ์ค„ ์„น์…˜์˜ ๊ฐœ์ˆ˜๋„ ๋™์ผํ•  ์ˆ˜ ์žˆ๋„๋ก PersistentManager์˜ notes ๊ด€๋ฆฌ(๋ฐฐ์—ด ์š”์†Œ ์ œ๊ฑฐ, ์ฝ”์–ด๋ฐ์ดํ„ฐ ์š”์†Œ ์ œ๊ฑฐ)๋„ ๋นผ๋จน์ง€ ์•Š๋„๋ก ํ•ด์ฃผ์—ˆ๋‹ค.

2-4 ๋ฐฐ์šด ๊ฐœ๋…

tableView์˜ Delegate ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•œ ์Šค์™€์ดํ”„ ๊ธฐ๋Šฅ ํ™œ์šฉ

[tableView์˜ Delegate ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•œ ์Šค์™€์ดํ”„ ๊ธฐ๋Šฅ ํ™œ์šฉ]

tableView์˜ Delegate ๋ฉ”์„œ๋“œ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์…€์„ ์Šค์™€์ดํ”„ ํ–ˆ์„ ๋•Œ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜์„ ์„ค์ • ํ•  ์ˆ˜ ์žˆ๋‹ค.

// ์˜ค๋ฅธ์ชฝ์—์„œ ์™ผ์ชฝ์œผ๋กœ ์Šค์™€์ดํ”„ ํ–ˆ์„ ๋•Œ์˜ ์˜ต์…˜ ์„ค์ •
override func tableView(
        _ tableView: UITableView,
        trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath
    ) -> UISwipeActionsConfiguration? {
        // ...
    }
    
// ์™ผ์ชฝ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ ์Šค์™€์ดํ”„ ํ–ˆ์„ ๋•Œ์˜ ์˜ต์…˜ ์„ค์ •
override func tableView(_ tableView: UITableView, 
    leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath
    ) -> UISwipeActionsConfiguration? {
        // ...
    }
Core Data - Codegen

[Codegen]

https://developer.apple.com/documentation/coredata/modeling_data/generating_code

ํ”„๋กœ์ ํŠธ ํ•˜๋Š” ๋„์ค‘์— codegen์„ ์–ด๋–ค ์˜ต์…˜์œผ๋กœ ์ค˜์•ผํ• ์ง€ ๊ฐ์ด ์•ˆ์™€์„œ ์ฐพ์•„๋ณด์•˜๋‹ค.

  • ํ•ด๋‹น entity์— ๋Œ€ํ•œ ํด๋ž˜์Šค ์„ ์–ธ์„ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด ์ฃผ๋Š” ์˜ต์…˜์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
    • None/Manual: ๊ด€๋ จ ํŒŒ์ผ์„ ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์ฃผ์ง€ ์•Š๋Š”๋‹ค. ๊ฐœ๋ฐœ์ž๋Š” DataModel์„ ์„ ํƒํ•œ ์ƒํƒœ์—์„œ Editor-Create NSManagedObject Subclass ํ•ญ๋ชฉ์„ ํด๋ฆญํ•˜์—ฌ ํด๋ž˜์Šค ์„ ์–ธ ํŒŒ์ผ๊ณผ ํ”„๋กœํผํ‹ฐ extension ํŒŒ์ผ์„ ๋นŒ๋“œ์‹œ๋งˆ๋‹ค ์ถ”๊ฐ€์‹œ์ผœ ์ฃผ๊ณ , ์ด๋ฅผ ์ˆ˜๋™์œผ๋กœ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.
    • Class Definition: ํด๋ž˜์Šค ์„ ์–ธ ํŒŒ์ผ๊ณผ ํ”„๋กœํผํ‹ฐ ๊ด€๋ จ extension ํŒŒ์ผ์„ ๋นŒ๋“œ์‹œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ถ”๊ฐ€์‹œ์ผœ์ค€๋‹ค. ๋”ฐ๋ผ์„œ ๊ด€๋ จ๋œ ํŒŒ์ผ์„ ์ „ํ˜€ ์ถ”๊ฐ€์‹œ์ผœ์ค„ ํ•„์š”๊ฐ€ ์—†๋‹ค.(๊ทธ๋ž˜์„œ๋„ ์•ˆ๋œ๋‹ค. ๋งŒ์•ฝ ์ˆ˜๋™์œผ๋กœ ์ถ”๊ฐ€์‹œ์ผœ์ค€ ์ƒํƒœ์—์„œ ๋นŒ๋“œ๋ฅผ ์‹œ๋„ํ•˜๋ฉด ์ปดํŒŒ์ผ ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค.)
    • Category/Extension: ํ”„๋กœํผํ‹ฐ ๊ด€๋ จ extensionํŒŒ์ผ๋งŒ ์ž๋™์œผ๋กœ ์ถ”๊ฐ€์‹œ์ผœ ์ค€๋‹ค. ์ฆ‰, ํด๋ž˜์Šค ์„ ์–ธ์—๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ๋กœ์ง์„ ์ž์œ ๋กญ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋‹ค.
NSFetchRequest - returnsObjectsAsFaults

[NSFetchRequest - returnsObjectsAsFaults]

https://developer.apple.com/documentation/coredata/nsfetchrequest/1506756-returnsobjectsasfaults

  • CoreData์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ชจ๋ธ์„ ์„ค๊ณ„ํ•˜๋‹ค๊ฐ€ ์ด๋Ÿฐ ํ”„๋กœํผํ‹ฐ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋˜์—ˆ๋‹ค.
  • ๊ธฐ๋ณธ๊ฐ’์€ true์ธ ์†์„ฑ์ด๋‹ค.
  • true์ธ ๊ฒฝ์šฐ Request๋กœ ๊ฐ€์ ธ์˜จ ๊ฐ์ฒด๊ฐ€ Faulting์ธ ๊ฒฝ์šฐ๋ผ๊ณ  ํ•œ๋‹ค.

Faulting ์˜ˆ์‹œ

  • ์‚ฌ์ง„๊ณผ ๊ฐ™์ด Department๋Š” ํด๋ž˜์Šค ์ธ์Šคํ„ด์Šค์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ์ธ์Šคํ„ด์Šค๊ฐ€ ์ƒ์„ฑ๋˜์–ด์žˆ์ง€๋งŒ, ์†์„ฑ์€ ๋น„์–ด์žˆ๋Š” ์ƒํƒœ์ด๋‹ค. ์ด ์ƒํƒœ๋ฅผ ๊ฒฐํ•จ์ด ์žˆ๋‹ค๋ผ๊ณ  ๋ณธ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. (์ด๋ฅผ ์˜ค๋ฅ˜๋ผ๊ณ  ํ•จ) ๋ถ€์„œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋‹ˆ ์ง์› ์ธ์Šคํ„ด์Šค๋„ ์ƒ์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์„ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ด€๊ณ„๋„ ์ฑ„์šธํ•„์š”๋„ ์—†์Œ์„ ์˜๋ฏธํ•œ๋‹ค.
  • ๊ทธ๋ž˜ํ”„๊ฐ€ ์™„์ „ ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ์ง์›์˜ ํ”„๋กœํผํ‹ฐ๋ฅผ ํŽธ์ง‘ํ•˜๋ ค๋ฉด ๊ถ๊ทน์ ์œผ๋กœ ์ „์ฒด ๊ธฐ์—… ๊ตฌ์กฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐœ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์•ผํ•œ๋‹ค.

๋”ฐ๋ผ์„œ returnsObjectsAsFaults๊ฐ€ true์ธ ๊ฒฝ์šฐ ์œ„์™€ ๊ฐ™์€ ๊ฒฐํ•จ์„ ๊ฐ€์ง€๊ณ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋„ ์œ„ ๊ทธ๋ฆผ๊ณผ ๊ฐ™์€ Department ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์ง€์•Š๋Š”๋‹ค๋Š” ์ด์•ผ๊ธฐ์ธ ๊ฒƒ ๊ฐ™๋‹ค. ์ฆ‰ ๊ฒฐํ•จ์„ ํ—ˆ์šฉํ•˜๊ฒ ๋‹ค๋Š” ์˜๋ฏธ์ธ๊ฑธ๊นŒ? false์ธ ๊ฒฝ์šฐ์—๋Š” ๊ฒฐํ•จ์ด ์žˆ๋˜ ๋ง๋˜ ๋ชจ๋“  ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ๊ฐ•์ œํ•œ๋‹ค๋Š” ๋œป์ธ ๊ฒƒ ๊ฐ™๋‹ค.

  • ๋ญ” ๋ง์ธ์ง€ ์ž˜ ์ดํ•ด๊ฐ€ ๊ฐ€์ง€ ์•Š์•„์„œ ์ข€๋” ๊ณต๋ถ€๊ฐ€ ํ•„์š”ํ•  ๊ฒƒ ๊ฐ™๋‹ค...
  • ์ค‘์š”ํ•œ ๊ฒƒ์€ returnsObjectsAsFaults ์ด ํ”Œ๋ž˜๊ทธ๊ฐ€ CoreData์— ๋งค์šฐ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ธ Lazy loading๋ฅผ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์ง€์‹œํ•œ๋‹ค๊ณ  ํ•œ๋‹ค. [?]
NSFetchRequestResult

[Protocol - NSFetchRequestResult]

https://developer.apple.com/documentation/coredata/nsfetchrequestresult

  • FetchRequest๋ฅผ ๋ณด๋‚ผ๋•Œ ๋‹จ์ˆœํžˆ NSManagedObject๋ง๊ณ  ๋‹ค๋ฅธ ํƒ€์ž…๋“ค๋„ ์œ ์—ฐํ•˜๊ฒŒ ๋ฐ›๊ณ ์‹ถ๋‹ค๋ฉด, ์ด ํ”„๋กœํ† ์ฝœ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
  • Conforming Types
    • NSDictionary
    • NSManagedObject
    • NSManagedObjectID
    • NSNumber
UIContextualAction์— ํ…์ŠคํŠธ๋ง๊ณ  ์•„์ด์ฝ˜ ์‚ฝ์ž…ํ•˜๋Š” ๋ฐฉ๋ฒ•

[UIContextualAction์— ํ…์ŠคํŠธ๋ง๊ณ  ์•„์ด์ฝ˜ ์‚ฝ์ž…ํ•˜๋Š” ๋ฐฉ๋ฒ•]

let deleteAction = UIContextualAction(style: .destructive, title: nil) { _, _, completeHandeler in
    self.deleteCell(indexPath: indexPath)
    completeHandeler(true)
}
deleteAction.image = UIImage(systemName: "trash.fill")
  • ๋จผ์ € UIContextualAction ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.
  • ์ƒ์„ฑํ•  ๋•Œ title์ด nil์ธ๊ฒŒ ํฌ์ธํŠธ์ด๋‹ค.
  • ์ดํ›„ ์ƒ์„ฑํ•œ UIContextualAction์— image๋ฅผ ๋Œ€์ž…ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

์ ์šฉ๋œ ๋ชจ์Šต

์˜๋ฌธ์  UIContextualAction ํŒŒ๋ผ๋ฏธํ„ฐ ์ค‘ handler์˜ ์šฉ๋„๋Š” ๋ฌด์—‡์ผ๊นŒ? https://developer.apple.com/documentation/uikit/uicontextualaction/handler

  • handler์˜ ์ž‘์—…์ด ์‹ค์ œ๋กœ ์ˆ˜ํ–‰๋œ ๊ฒฝ์šฐ ํ•ธ๋“ค๋Ÿฌ์— true๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ์ž‘์—…์ด ์™„๋ฃŒ๋˜์—ˆ๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๋ ค์ฃผ๋Š” ์šฉ๋„๋ผ๊ณ  ํ•œ๋‹ค.
  • ์ง€๊ธˆ๊ฐ™์ด ๊ฐ„๋‹จํ•œ ๋กœ์ง์ธ ๊ฒฝ์šฐ ๊ทธ๋ƒฅ true๋กœ ๊ธฐ์ž…ํ•ด์ฃผ๋ฉด ๋˜๊ฒ ์ง€๋งŒ, ๋งŒ์•ฝ ๋ณต์žกํ•œ ๋กœ์ง์ด ์ถ”๊ฐ€๋˜์–ด ์—๋Ÿฌ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค˜์•ผํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” false๋ฅผ ์ „๋‹ฌํ•˜์—ฌ ์ž‘์—…์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์„ ์•Œ๋ฆฌ๋Š” ์šฉ๋„์ธ ๊ฒƒ ๊ฐ™๋‹ค.
  • https://developer.apple.com/forums/thread/129420
  • ์—ฌ๊ธฐ ๊ธ€์„ ์ฐธ๊ณ ํ•˜๋‹ˆ ํ˜„์žฌ๋Š” completeHandeler๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์ง€๋งŒ, ๋‚˜์ค‘์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ ์ ˆํ•œ ๊ฐ’์„ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•œ๋‹ค๋Š” ๋‹ต๋ณ€์ด ์žˆ๋‹ค.
  • ๊ทธ๋ž˜์„œ ํŒ€์›๋“ค๊ณผ ์˜๋…ผํ•˜์—ฌ true๋กœ ๊ธฐ์ž…ํ•ด์ฃผ๊ธฐ๋กœ ํ•˜์˜€๋‹ค.
UIAlertAction ํŽธ์ง‘ํ•˜๊ธฐ(titleTextAlignment, image)

[UIAlertAction ํŽธ์ง‘ํ•˜๊ธฐ]

๋‹จ์ˆœํ•œ ๊ธ€์ž๋ง๊ณ  ์—ฌ๊ธฐ์—๋„ ์•„์ด์ฝ˜๊ฐ™์€๊ฑธ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์„๊นŒ?

let deleteAction = UIAlertAction(title: "Delete", style: .destructive, handler: deleteHandler)
let deleteImage = UIImage(systemName: "trash.fill")
deleteAction.setValue(deleteImage, forKey: "image")
deleteAction.setValue(0, forKey: "titleTextAlignment")
  • setValue ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด์„œ ์ง€์ •ํ•ด์ค„ ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • ๋‹จ์ˆœํ•˜๊ฒŒ ์ด๋ฏธ์ง€์™€ ํ…์ŠคํŠธ์˜ alignment๋ฅผ ์ง€์ •ํ•ด์ฃผ์—ˆ๋‹ค.
    • 0 - left
    • 1 - center
    • 2 - right

์ ์šฉ๋œ ๋ชจ์Šต

2-5 PR ํ›„ ๊ฐœ์„ ์‚ฌํ•ญ

  • flatMap์„ ํ™œ์šฉํ•˜์—ฌ ์˜ต์…”๋„ ๋ฐ”์ธ๋”ฉ ๋ถ€๋ถ„์„ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ๊ฐœ์„ 
  • primary, secondary ์„œ๋กœ์˜ ์ด๋ฒคํŠธ ์ „๋‹ฌ์„ SplitViewController๊ฐ€ ์•„๋‹ˆ๋ผ Delegate ํŒจํ„ด์œผ๋กœ ๊ฐœ์„ 
  • ๊ฐ€๋…์„ฑ์ด ๋–จ์–ด์ง€๋Š” ๋ฉ”์†Œ๋“œ๋ฅผ ๋ฉ”์†Œ๋“œ๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ฐœ์„ 
  • ์…€์„ ์„ ํƒํ•˜๊ณ  ์Šค์™€์ดํ”„ ํ–ˆ์„ ๋•Œ ์„ ํƒ์ด ํ•ด์ œ๋˜๋Š” ๋ฒ„๊ทธ ๊ฐœ์„ 
  • ๋”๋ณด๊ธฐ ๋ฒ„ํŠผ์—์„œ ๋ฉ”๋ชจ๋ฅผ ์‚ญ์ œํ•œ ํ›„ ๋‹ค์Œ ๋ฉ”๋ชจ์žฅ์„ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ฐœ์„ 

top

STEP 3 : ํด๋ผ์šฐ๋“œ ์—ฐ๋™

SwiftyDropbox ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋ฉ”๋ชจ์žฅ๊ณผ ํด๋ผ์šฐ๋“œ ์—ฐ๋™์„ ํ•ฉ๋‹ˆ๋‹ค.

3-1 ๊ณ ๋ฏผํ–ˆ๋˜ ๊ฒƒ

1. ์—…๋กœ๋“œ ์‹œ์ 

  • ์—…๋กœ๋“œ ํ•˜๋Š” ์‹œ์ ์„ ์ •ํ•  ๋•Œ ๊ณ ๋ คํ•œ ๊ฒƒ์€ ๋„ˆ๋ฌด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์—…๋กœ๋“œ๋ฅผ ํ•˜์ง€ ์•Š์•„์•ผ ํ•˜๋ฉฐ, ์•ฑ์ด ์˜๋„์น˜ ์•Š๊ฒŒ ์ข…๋ฃŒ๋˜์–ด๋„ ์–ด๋Š์ •๋„ ๋ฐฉ์–ด๊ฐ€ ๋˜์–ด์•ผ ํ•œ๋‹ค๋Š” ์ ์ด์—ˆ๋‹ค.
  • ์ฒ˜์Œ์—๋Š” ํ…์ŠคํŠธ๋ทฐ์— ๋ณ€ํ™”๊ฐ€ ์ผ์–ด๋‚  ๋•Œ๋งˆ๋‹ค ํ•˜๋ ค๊ณ  ํ•˜์˜€์œผ๋‚˜ ๋„ˆ๋ฌด ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚  ๊ฒƒ์œผ๋กœ ๋ณด์˜€๋‹ค. ๊ทธ๋ž˜์„œ ์ข…๋ฃŒํ•  ๋•Œ ํ•˜๋ ค๊ณ  ํ•˜๋‹ˆ ์˜๋„์น˜ ์•Š์€ ์ข…๋ฃŒ์— ์ „ํ˜€ ๋ฐฉ์–ด๊ฐ€ ๋˜์ง€ ์•Š์•˜๋‹ค.
  • ์—ฌ๋Ÿฌ ๊ณ ๋ฏผ์„ ํ•œ ๊ฒฐ๊ณผ ํ‚ค๋ณด๋“œ๋ฅผ ๋‚ด๋ฆด ๋•Œ ๋งˆ๋‹ค ์—…๋กœ๋“œ๋ฅผ ํ•˜๋„๋ก ํ•˜์—ฌ ์—…๋กœ๋“œ๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ผ์–ด๋‚˜์ง€ ์•Š๋„๋ก ํ•˜์˜€๊ณ  ์˜๋„์น˜ ์•Š๊ฒŒ ์ข…๋ฃŒ๋˜์–ด๋„ ๋ฐฉ์–ด๋ฅผ ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜์˜€๋‹ค.

2. ๋‹ค์šด๋กœ๋“œ์˜ ์‹œ์ 

  • ๋‹ค์šด๋กœ๋“œ ์‹œ์ ์€ ์ฒ˜์Œ์— ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์„ ์„ฑ๊ณตํ•˜๋Š” ์‹œ์ ์— dropbox์˜ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์šด๋กœ๋“œ ํ•˜์—ฌ ๋ณด์—ฌ์ฃผ๋Š” ์ฃผ๋„๋ก ๊ตฌํ˜„ํ•˜๊ธธ ์›ํ–ˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ธ์ฆ์ด ์™„๋ฃŒ ๋˜๊ณ  authResult(์ธ์ฆ๊ฒฐ๊ณผ)๊ฐ€ success๊ฐ€ ๋˜๋ฉด download๋ฅผ ํ•˜๋„๋ก ํ•˜์˜€๋‹ค.
  • ๋˜ํ•œ, ๋‹ค์šด๋กœ๋“œ๋Š” ๋น„๋™๊ธฐ๋กœ ์ง„ํ–‰์ด ๋˜๊ธฐ ๋–„๋ฌธ์— DispatchGroup์„ ์‚ฌ์šฉํ•˜์—ฌ ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด ์•ฑ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋ฟŒ๋ ค์ฃผ๊ณ  ํ…Œ์ด๋ธ”๋ทฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

3. ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์ง„ํ–‰์ค‘์ผ ๋•Œ ๋ทฐ์˜ ์ƒํƒœ

  • ๋‹ค์šด๋กœ๋“œ๊ฐ€ ์ง„ํ–‰๋  ๋™์•ˆ ๋ทฐ๋Š” ์•„๋ฌด๊ฒƒ๋„ ๋ณด์—ฌ์ฃผ์ง€ ์•Š๊ฒŒ ๋œ๋‹ค.
  • ๋กœ๋”ฉ์ค‘์ด๋ผ๋Š” ๊ฒƒ์„ ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ ค์ฃผ๊ธฐ ์œ„ํ•ด์„œ ActivityIndicator๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค.
  • ๋‹ค์šด๋กœ๋“œ๋ฅผ ์š”์ฒญํ•˜๊ณ  ActivityIndicator๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ฃผ๋„๋ก ํ•˜๊ณ  ๋‹ค์šด๋กœ๋“œ๊ฐ€ ๋ชจ๋‘ ์™„๋ฃŒ๋˜๋ฉด ActivityIndicator๋Š” ์ข…๋ฃŒ๋˜๋ฉฐ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ๋ณด์—ฌ์ฃผ๋„๋ก ๊ตฌํ˜„ํ•˜์˜€๋‹ค.

3-2 ์˜๋ฌธ์ 

  • ScopeRequest์˜ scopes๋Š” ๋ญ˜๊นŒ?
  • CoreData์˜ ๊ฒฝ๋กœ๋Š” ์–ด๋””์ผ๊นŒ?
  • CoreData๋ฅผ ๋ฎ์–ด์“ฐ๊ธฐ๊ฐ€ ์•„๋‹ˆ๋ผ ์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ ์—…๋ฐ์ดํŠธ ํ•ด์ค„ ์ˆ˜๋Š” ์—†์„๊นŒ?
  • wal, shm์ด ๋ฌด์Šจ๋œป์ผ๊นŒ?

3-3 Trouble Shooting

  • 1. download๊ฐ€ ๋๋‚˜๋Š” ์‹œ์ ์— ๋ทฐ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๊ธฐ

๋‹ค์šด๋กœ๋“œ๊ฐ€ ๋๋‚œ ํ›„ CoreData๋ฅผ fetch๋ฅผ ํ•˜๊ณ  TableView๋ฅผ reload๋ฅผ ํ•ด์ฃผ๊ณ  ์‹ถ์—ˆ์œผ๋‚˜ ์‹คํŒจํ–ˆ์—ˆ๋‹ค.

  • ์ด์œ  ํŒŒ์ผ์ด ์—ฌ๋Ÿฌ๊ฐœ๊ฐ€ ์กด์žฌํ•˜์—ฌ, ์—ฌ๋Ÿฌ๊ฐœ์˜ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œ ํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ˜๋ณต๋ฌธ์„ ๋Œ๋ฆฌ๊ณ  ์žˆ์—ˆ์œผ๋‚˜, fetch์™€ reload๋ฅผ for-in๋ฌธ ๋‚ด๋ถ€์—์„œ ํ•ด์ฃผ๊ณ  ์žˆ์–ด์„œ, ๋ทฐ๊ฐ€ ์—…๋ฐ์ดํŠธ ๋  ๋•Œ๊ฐ€ ์žˆ๊ณ , ์•ˆ๋˜๊ธฐ๋„ ํ•˜๋Š” ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚ฌ๋‹ค.

* `ํ•ด๊ฒฐ` ๊ทธ๋ž˜์„œ `for-in๋ฌธ์ด ์ข…๋ฃŒ๋œ ์‹œ์ `์— `fetch`๋ฅผ ํ•˜๊ณ  view๋ฅผ reload๋ฅผ ํ•ด์ฃผ๊ธฐ ์œ„ํ•ด, ๋‹ค์šด๋กœ๋“œ๊ฐ€ ๋ชจ๋‘ ์™„๋ฃŒ๋˜๋Š” ์‹œ์ ์„ `DispatchGroup`๋ฅผ ํ™œ์šฉํ•˜์—ฌ `์ถ”์ `ํ•˜๊ณ , ๋ฐ˜๋ณต๋ฌธ์—์„œ ์‹œ์ž‘๋˜์—ˆ๋˜ ๋‹ค์šด๋กœ๋“œ ์ž‘์—…์ด ๋ชจ๋‘ ๋๋‚˜๊ฒŒ ๋˜๋ฉด ์•„๋ž˜ ๋ทฐ๋ฅผ ๋‹ค์‹œ ์„ค์ •ํ•˜๋„๋ก ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜์˜€๋‹ค.
func download(_ tableViewController: NotesViewController?) {
    let group = DispatchGroup() // ๊ทธ๋ฃน ์ƒ์„ฑ
    for fileName in fileNames {
        let destURL = applicationSupportDirectoryURL.appendingPathComponent(fileName)
        let destination: (URL, HTTPURLResponse) -> URL = { _, _ in
            return destURL
        }
        group.enter() // ์ž‘์—… ์‹œ์ž‘
        client?.files.download(path: fileName, overwrite: true, destination: destination)
            .response { _, error in
                if let error = error {
                    print(error)
                }
                group.leave() // ์ž‘์—… ๋
            }
    }
    group.notify(queue: .main) { // ๋ชจ๋“  ์ž‘์—…์ด ๋๋‚œ๋‹ค๋ฉด ...
        PersistentManager.shared.setUpNotes()
        tableViewController?.tableView.reloadData()
        tableViewController?.stopActivityIndicator()
    }
}

3-4 ๋ฐฐ์šด ๊ฐœ๋…

[Swift Package Manager๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ™œ์šฉํ•˜๊ธฐ]

Targets -> General -> Frameworks, Libraries, and Embedded Content -> +

Add Package Dependency... ๋ฅผ ํด๋ฆญ

์‚ฌ์šฉํ•˜๊ณ  ์‹ถ์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ ์ฃผ์†Œ๋ฅผ ๊ธฐ์ž…ํ•œ๋‹ค.

์„ค์น˜ ์‹œ ์›ํ•˜๋Š” ๋ฒ„์ „, ๋ธŒ๋žœ์น˜ ๋ฐ ์ปค๋ฐ‹์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด ํ›„ ์›ํ•˜๋Š” packge product๋ฅผ ๊ณจ๋ผ์„œ Finish ๊นŒ์ง€ ํ•˜๋ฉด...

SwiftyDropbox๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์„ค์น˜๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

[ํ”„๋กœ์ ํŠธ์— SwiftyDropbox ์„ค์ •ํ•˜๊ธฐ]

์•„๋ž˜ ํ”„๋กœ์ ํŠธ ์„ค์ •ํ•˜๋Š” ํŠœํ† ๋ฆฌ์–ผ์„ ์ฐธ๊ณ ํ•˜์—ฌ ์ง„ํ–‰ํ•˜์˜€๋‹ค.

https://github.com/dropbox/SwiftyDropbox#configure-your-project

๋จผ์ € Info.plist ํŒŒ์ผ์„ ์ˆ˜์ •ํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š”๋ฐ, ๊ทธ ์ „์— dropbox์— app์„ ๋“ฑ๋กํ•ด์•ผ ํ•œ๋‹ค. ๋กœ๊ทธ์ธ ํ›„ apps์— ๋“ค์–ด๊ฐ€๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฒ„ํŠผ์ด ์žˆ๋‹ค.

์ดํ›„ ํ•„์ˆ˜ ๋ฌธํ•ญ์„ ์„ ํƒ, ์ž…๋ ฅ ํ›„ create app ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ๋งŒ๋“ค์–ด์ฃผ๋ฉด ๋œ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด App key๊ฐ€ ๋ฐœ๊ธ‰๋˜๋Š”๋ฐ, ์ด๊ฑธ ์ด์ œ Info.plist๋ฅผ ์ˆ˜์ •ํ•˜๋Š”๋ฐ ํ™œ์šฉํ•  ๊ฒƒ์ด๋‹ค.

ํŠœํ† ๋ฆฌ์–ผ์—์„œ ํ•˜๋ผ๋Š”๋ฐ๋กœ Info.plist๋ฅผ ์˜ˆ์‹œ์™€ ๊ฐ™์ด ์ˆ˜์ •ํ•ด์ค€๋‹ค.

<key>LSApplicationQueriesSchemes</key>
    <array>
        <string>dbapi-8-emm</string>
        <string>dbapi-2</string>
    </array>

์•„๊นŒ ๋งŒ๋“ค๊ณ  ์–ป์€ App key๋ฅผ db- ๋’ค๋ถ€ํ„ฐ ๊ธฐ์ž…ํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

<key>CFBundleURLTypes</key>
    <array>
        <dict>
            <key>CFBundleURLSchemes</key>
            <array>
                <string>db-<APP_KEY></string>
            </array>
            <key>CFBundleURLName</key>
            <string></string>
        </dict>
    </array>

์ดํ›„ ์ฝ”๋“œ๋กœ ๋Œ์•„๊ฐ€์„œ AppDelegate์— DropboxClient ์ธ์Šคํ„ด์Šค๋ฅผ ์ดˆ๊ธฐํ™” ํ•ด์ค€๋‹ค.

import SwiftyDropbox

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    DropboxClientsManager.setupWithAppKey("<APP_KEY>")
    return true
}

๊ทธ๋ฆฌ๊ณ  SceneDelegate์— ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฉ”์†Œ๋“œ๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

import SwiftyDropbox

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
     let oauthCompletion: DropboxOAuthCompletion = {
      if let authResult = $0 {
          switch authResult {
          case .success:
              print("Success! User is logged into DropboxClientsManager.")
          case .cancel:
              print("Authorization flow was manually canceled by user!")
          case .error(_, let description):
              print("Error: \(String(describing: description))")
          }
      }
    }

    for context in URLContexts {
        // stop iterating after the first handle-able url
        if DropboxClientsManager.handleRedirectURL(context.url, completion: oauthCompletion) { break }
    }
}
    }

์ดํ›„ ๋งจ์ฒ˜์Œ์— ์‹œ์ž‘ํ•˜๋Š” ๋ทฐ์— ๋กœ๊ทธ์ธ์„ ํ•ด์„œ ์ธ์ฆ ํ† ํฐ์„ ๋ฐ›์•„์˜ค๋Š” ์ž‘์—…์„ ์ถ”๊ฐ€ํ•œ๋‹ค. ์ด๋ฒˆ ํ”„๋กœ์ ํŠธ ๊ฐ™์€ ๊ฒฝ์šฐ UISplitViewController๋ฅผ ์‚ฌ์šฉํ–ˆ๋Š”๋ฐ, rootView์ธ SplitViewController์—์„œ๋Š” ํ•ด๋‹น ์ž‘์—…์ด ์ •์ƒ์ ์œผ๋กœ ๋œจ์ง€์•Š์•˜๋‹ค. (์ด์œ ๋Š” ์ฐพ์ง€ ๋ชปํ–ˆ๋‹ค.) ๊ทธ๋ž˜์„œ ๋‹ค๋ฅธ UIViewController์—์„œ ์ง„ํ–‰ํ•ด์•ผํ•˜๋‚˜.. ์‹ถ์–ด์„œ UITableViewController์˜ viewDidLoad()์—์„œ ํ•ด๋‹น ์ž‘์—…์„ ์‹คํ–‰ํ•ด์ฃผ๋‹ˆ ๋กœ๊ทธ์ธ์ฐฝ์ด ์ •์ƒ์ ์œผ๋กœ ๋–ด๋‹ค.

import SwiftyDropbox

func myButtonInControllerPressed() {
    // OAuth 2 code flow with PKCE that grants a short-lived token with scopes, and performs refreshes of the token automatically.
    let scopeRequest = ScopeRequest(scopeType: .user, scopes: ["account_info.read"], includeGrantedScopes: false)
    DropboxClientsManager.authorizeFromControllerV2(
        UIApplication.shared,
        controller: self,
        loadingStatusDelegate: nil,
        openURL: { (url: URL) -> Void in UIApplication.shared.open(url, options: [:], completionHandler: nil) },
        scopeRequest: scopeRequest
    )

    // Note: this is the DEPRECATED authorization flow that grants a long-lived token.
    // If you are still using this, please update your app to use the `authorizeFromControllerV2` call instead.
    // See https://dropbox.tech/developers/migrating-app-permissions-and-access-tokens
    DropboxClientsManager.authorizeFromController(UIApplication.shared,
                                                  controller: self,
                                                  openURL: { (url: URL) -> Void in
                                                    UIApplication.shared.open(url, options: [:], completionHandler: nil)
                                                  })
}

์—ฌ๊ธฐ์„œ scopes๋ผ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ๋Š”๋ฐ, ์ด ๋ถ€๋ถ„์€ ์•ฑ์ด Dropbox ๊ณ„์ • ์ •๋ณด๋ฅผ ๋ณด๊ณ  ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ถŒํ•œ์˜ ๋ฒ”์œ„๋ฅผ ๋œปํ•œ๋‹ค. ์•„๊นŒ App key๋ฅผ ์–ป์—ˆ๋˜ ๊ณณ์—์„œ Permissions ํƒญ์„ ํด๋ฆญํ•˜๋ฉด Account์˜ ์ •๋ณด๊ฐ€ ๋‚˜์˜จ๋‹ค. ๋”ฐ๋ผ์„œ ํ•„์š”ํ•œ Account๋ฅผ scopes์— ๋„ฃ์–ด์ฃผ๋ฉด ๋˜๊ฒ ๋‹ค.

์ด ๋‹ค์Œ์— API์— ํ˜ธ์ถœํ•  DropboxClient ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

import SwiftyDropbox

// Reference after programmatic auth flow
let client = DropboxClientsManager.authorizedClient

client๋ฅผ ํ†ตํ•ด ์—…๋กœ๋“œ์™€ ๋‹ค์šด๋กœ๋“œ๋ฅผ ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

let fileData = "testing data example".data(using: String.Encoding.utf8, allowLossyConversion: false)!

let request = client.files.upload(path: "/test/path/in/Dropbox/account", input: fileData)
    .response { response, error in
        if let response = response {
            print(response)
        } else if let error = error {
            print(error)
        }
    }
    .progress { progressData in
        print(progressData)
    }

// in case you want to cancel the request
if someConditionIsSatisfied {
    request.cancel()
}
// Download to URL
let fileManager = FileManager.default
let directoryURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
let destURL = directoryURL.appendingPathComponent("myTestFile")
let destination: (URL, HTTPURLResponse) -> URL = { temporaryURL, response in
    return destURL
}
client.files.download(path: "/test/path/in/Dropbox/account", overwrite: true, destination: destination)
    .response { response, error in
        if let response = response {
            print(response)
        } else if let error = error {
            print(error)
        }
    }
    .progress { progressData in
        print(progressData)
    }


// Download to Data
client.files.download(path: "/test/path/in/Dropbox/account")
    .response { response, error in
        if let response = response {
            let responseMetadata = response.0
            print(responseMetadata)
            let fileContents = response.1
            print(fileContents)
        } else if let error = error {
            print(error)
        }
    }
    .progress { progressData in
        print(progressData)
    }

๋‘๊ฐ€์ง€์˜ ๊ณตํ†ต์ ์€ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ , ์—…๋กœ๋“œํ•  ๋•Œ ๊ฒฝ๋กœ๊ฐ€ ํ•„์š”ํ•˜๋‹ค๋Š” ์ ์ด๋‹ค. ๋ฉ”๋ชจ์žฅ ํ”„๋กœ์ ํŠธ์˜ ๊ฒฝ์šฐ CoreData๋ฅผ ํ†ตํ•ด์„œ ๋ฉ”๋ชจ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฐฑ์—…์˜ ํ˜•ํƒœ๋กœ CoreData์˜ ๊ฒฝ๋กœ๋ฅผ ์–ป์–ด๋‚ด์„œ .sqlite, .sqlite-shm, .sqlite-wal ์ด 3๊ฐœ์˜ ํŒŒ์ผ์„ ์—…๋กœ๋“œ ๋ฐ ๋‹ค์šด๋กœ๋“œ ํ•ด์ฃผ๋„๋ก ๊ตฌํ˜„ํ•ด์ฃผ์—ˆ๋‹ค.

์—…๋กœ๋“œ, ๋‹ค์šด๋กœ๋“œ ๋ชจ๋‘ ํŒŒ์ผ์„ ๋ฎ์–ด์“ธ๊ฑด์ง€์— ๋Œ€ํ•œ ์˜ต์…˜์ด ์žˆ์œผ๋‹ˆ ์ž์„ธํ•œ๊ฑด ์•„๋ž˜ ๋„ํ๋จผํŠธ์—์„œ ๊ฒ€์ƒ‰ํ•ด๋ณด๋ฉด ๋˜๊ฒ ๋‹ค.

https://dropbox.github.io/SwiftyDropbox/api-docs/latest/index.html

3-5 PR ํ›„ ๊ฐœ์„ ์‚ฌํ•ญ

  • ๋‹ค์šด๋กœ๋“œ ์ค‘์ผ ๋•Œ ํ„ฐ์น˜๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ indicator๊ฐ€ ์ถ”๊ฐ€๋œ ์ปจํŠธ๋กค๋Ÿฌ๋กœ ํ™”๋ฉด์„ ๋ฎ๊ธฐ
  • ๋‹ค์šด๋กœ๋“œ, ์—…๋กœ๋“œ์˜ ์—๋Ÿฌ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ๋„๋ก ๊ฐœ์„ 
  • PersistentManager์˜ discardableResult ์˜ต์…˜์„ ์ œ๊ฑฐ
  • NotesViewController์˜ setEditing ๋‚ด๋ถ€ if๋ฌธ ๋กœ์ง์„ deleteCell ๋ฉ”์†Œ๋“œ๋กœ ์ด๋™

top

STEP 4 : ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ๋ฐ UI ๊ตฌํ˜„

  • ์ง€์—ญํ™”, ์ ‘๊ทผ์„ฑ, ๊ฒ€์ƒ‰๊ธฐ๋Šฅ ๋“ฑ์„ ์ถ”๊ฐ€ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

4-1 ๊ณ ๋ฏผํ–ˆ๋˜ ๊ฒƒ

1. ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

  • ๊ฒ€์ƒ‰์–ด๋ฅผ ์ž…๋ ฅํ•  ๋•Œ๋งˆ๋‹ค ๊ฒ€์ƒ‰์–ด์— ํ•ด๋‹นํ•˜๋Š” ๋ฉ”๋ชจ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋„๋ก NSPredicate๋ฅผ ํ™œ์šฉํ•˜์—ฌ fetchํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€
  • ๊ฒ€์ƒ‰์–ด๊ฐ€ ๋นˆ ๋ฌธ์ž์—ด("")์ด๋ผ๋ฉด ๋‹ค์‹œ ๋ฉ”๋ชจ์˜ ์ „์ฒด๋ชฉ๋ก์„ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„

2. ์ง€์—ญํ™”

  • ์–ธ์–ด ์˜์–ด๋ฅผ ๊ธฐ๋ณธ์„ค์ •์œผ๋กœ ํ•˜๊ณ  ํ•œ๊ตญ์–ด์— ๋Œ€ํ•ด์„œ๋„ ๋‹ค๊ตญ์–ดํ™”๋ฅผ ์ง€์›ํ•˜๋„๋ก ๊ตฌํ˜„
  • ๋‚ ์งœ ์‹œ๊ฐ„์€ ์‹œ์Šคํ…œ ์‹œ๊ฐ„์„ ๋”ฐ๋ฅด๋„๋ก ํ•˜๊ณ , ๋‚ ์งœ ํ˜•์‹์€ ์–ธ์–ด์— ๋”ฐ๋ผ ํฌ๋งท์„ ๋ณ€๊ฒฝ๋˜๋„๋ก ๊ตฌํ˜„

3. ์ ‘๊ทผ์„ฑ

  • ์šฐ์ธก ๋ฉ”๋ชจ ์ƒ์„ธ ๋‚ด์šฉ์˜ ๊ฒฝ์šฐ VoiceOver๊ฐ€ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ฝ์€ ํ›„, ํ…์ŠคํŠธ๋ฅผ ๋ฐ”๋กœ ์ฝ์–ด์ฃผ๋Š” ๊ฒƒ์„ ํ™•์ธํ•˜๊ณ , ๋ฉ”๋ชจ ๋‚ด์šฉ์ด๋ผ๋Š” accessibilityLabel์„ ์ถ”๊ฐ€ํ•˜์—ฌ, ๋ฉ”๋ชจ ํ…์ŠคํŠธ๋ฅผ ์ฝ๊ธฐ ์ „์— ์ข€ ๋” ํ™”๋ฉด์˜ ๊ตฌ์„ฑ์š”์†Œ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์‰ฝ๋„๋ก ๊ตฌํ˜„
  • View์˜ ํ…์ŠคํŠธ ์š”์†Œ๋“ค์— Dynamic Type์„ ์ ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์‚ฌ์ด์ฆˆ๋กœ ํ…์ŠคํŠธ ํฌ๊ธฐ๋ฅผ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋„๋ก ์œ ์—ฐ์„ฑ์„ ์ œ๊ณต

4-2 ์˜๋ฌธ์ 

  • Cancel์ด๋‚˜ OK ๊ฐ™์€ ๊ฒƒ๋“ค์€ ์ž๋™์œผ๋กœ ์ง€์—ญํ™”๊ฐ€ ๋˜์ง€ ์•Š๋Š”๊ฑธ๊นŒ?
  • UITableView์—๋Š” accessibilityLabel์„ ์ถ”๊ฐ€ํ•ด์ค„ ์ˆ˜ ์—†๋Š”๊ฑธ๊นŒ?
  • CoreData๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” fetch ๊ธฐ๋Šฅ์€ ์—ฌ๋Ÿฌ๋ฒˆํ•˜๋ฉด ๋น„์šฉ์ด ๋งŽ์ด ๋“œ๋Š”๊ฑธ๊นŒ...?
  • Dropbox์—์„œ ๋ฐฑ์—…ํ–ˆ๋˜ ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œํ•ด์„œ CoreData๋ฅผ ๋ฎ์–ด์“ฐ๋ฉด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—๋Ÿฌ๊ฐ€ ๋‚˜๋Š”๋ฐ, ์•ฑ์€ ์ •์ƒ์ ์œผ๋กœ ์ž‘๋™ํ•œ๋‹ค. ์ด ๋ถ€๋ถ„์€ ์‹ ๊ฒฝ์“ฐ์ง€ ์•Š์•„๋„ ๋ ๊นŒ?

4-3 Trouble Shooting

์ ‘๊ทผ์„ฑ์„ ์œ„ํ•ด Accessibility Inspector๋ฅผ ํ™œ์šฉ

  • Accessibility Inspector๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์ ‘๊ทผ์„ฑ์„ ์œ„ํ•ด Run Audit์„ ํ†ตํ•ด ๊ฐœ์„ ํ•  ํ•ญ๋ชฉ๋“ค์ด ์—†๋Š”์ง€ ๊ฒ€์ˆ˜ํ•˜์˜€๋‹ค.
  • ๊ทธ๋ฆฌ๊ณ  VoiceOver๋ฅผ ์ง์ ‘ ์‹คํ–‰ํ•ด์„œ ํ…Œ์ŠคํŠธํ•ด๋ณด๋ฉฐ ๋ถ€์กฑํ•œ ๋ถ€๋ถ„์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ด๋ณด์•˜๋‹ค.
  • ๋‹ค์ด๋‚˜๋ฏน ํƒ€์ž…์˜ ๊ฒฝ์šฐ๋„ ํ…์ŠคํŠธ ํฌ๊ธฐ๊ฐ€ ์œ ์—ฐํ•œ์ง€ ๊ฒ€์ˆ˜ํ•˜์˜€๋‹ค.

4-4 ๋ฐฐ์šด ๊ฐœ๋…

[Localization]

Localization

์ง€์—ญํ™”๋ž€?

  • ์ง€์—ญํ™”๋Š” ํ˜„์ง€ํ™”ํ•œ๋‹ค๋Š” ๋œป์„ ๊ฐ€์กŒ๋‹ค
  • ์ฆ‰, ํ•ด๋‹น ์–ธ์–ด์™€ ๋‚˜๋ผ ์ง€์—ญ์— ๋งž๊ฒŒ ์•ฑ์„ ์„ค์ •ํ•ด์ฃผ๋Š” ๊ฒƒ์„ ๋œปํ•œ๋‹ค.
  • ๊ตญ์ œํ™”(internationalization)๋ฅผ I18Nย orย i18n์œผ๋กœ, ์ง€์—ญํ™”(localization)๋ฅผย L10N์ด๋‚˜ย l10n์œผ๋กœ ํ‘œ๊ธฐํ•œ๋‹ค

์ง€์—ญํ™”์˜ ์ „์ œ์กฐ๊ฑด

  • ํ•ด๋‹น ์•ฑ์ด ์ง€์—ญํ™”๊ฐ€ ๋˜๋ ค๋ฉด ์—ฌ๋Ÿฌ ๊ตญ๊ฐ€์— ๋ฐฐํฌ๋˜์–ด ๊ตญ์ œํ™” ๋˜์–ด์žˆ๋Š” ์•ฑ์ด๋ผ๋Š” ์กฐ๊ฑด์ด ์žˆ์–ด์•ผ ํ•œ๋‹ค.
  • ํ•ด๋‹น ์•ฑ์ด ํ•œ๊ตญ์—์„œ๋งŒ ์‚ฌ์šฉ๋˜๋Š” ์•ฑ์ด๋ผ๋ฉด ์ง€์—ญํ™”๊ฐ€ ์˜๋ฏธ ์—†์„ ๊ฒƒ์ด๋‹ค.

์ง€์—ญํ™” ๊ฐ€๋Šฅํ•œ ์š”์†Œ

  • RTL, LTR (๋ฌธํ™”๊ถŒ์— ๋”ฐ๋ฅธ ์ฝ๊ธฐ/์“ฐ๊ธฐ ๋ฐฉ์‹), ์–ธ์–ด, ์‹œ๊ฐ„, ๋‚ ์งœ, ์ฃผ์†Œ, ํ™”ํ๋‹จ์œ„ ๋ฐ ํ†ตํ™”, ์ด๋ฏธ์ง€ ๋“ฑ๋“ฑ...

์ง€์—ญํ™”์™€ ์ ‘๊ทผ์„ฑ์˜ ๊ด€๊ณ„

  • ์ง€์—ญํ™”๋ฅผ ํ•จ์œผ๋กœ ์—ฌ๋Ÿฌ ๊ตญ๊ฐ€์™€ ์ง€์—ญ์—์„œ ํ•ด๋‹น ์•ฑ์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ฑ(accessibility)๊ฐ€ ์šฐ์ˆ˜ํ•ด์ง„๋‹ค.
  • ์ ‘๊ทผ์„ฑ์€ ์• ํ”Œ์˜ ๊ฐ€์žฅ ๊ฐ•์ ์ธ ๋ถ€๋ถ„์œผ๋กœ ๊ผญ ์ด ๋ถ€๋ถ„์„ ์ž˜ ํ™œ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•ด๋†“์œผ๋ฉด ์ข‹๋‹ค.
    • ์ ‘๊ทผ์„ฑ(accessibility)์„ ์„ค์ •ํ•˜๋ ค๋ฉด accessibility inspector๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ๊ฐ€์ง€๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

์–ธ์–ด ์ง€์—ญํ™”

  • ์ง€์—ญํ™” ํ•˜๋ ค๋Š” ์–ธ์–ด๋ฅผ ํ”„๋กœ์ ํŠธ์— ์ถ”๊ฐ€ํ•œ๋‹ค.
    • ํƒ€๊ฒŸ์„ ์„ ํƒํ•ด์„œ ๋‹ค๊ตญ์–ดํ™”

  • ์ฝ”๋“œ๋กœ ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ
    • Strings ํŒŒ์ผ์„ ์ƒ์„ฑํ•˜๊ณ 
      • Localizable.strings ๋กœ ๋„ค์ด๋ฐ ๋ณ€๊ฒฝ

  • Localize... ๋ฒ„ํŠผ ํด๋ฆญ

  • ๋‹ค์‹œ ํƒ€๊ฒŸ์œผ๋กœ ๋Œ์•„๊ฐ€์„œ ์ง€์—ญํ™”ํ•˜๊ณ  ์‹ถ์€ ์–ธ์–ด๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๊ธฐ

  • ์•„๊นŒ ๋งŒ๋“  ํŒŒ์ผ์„ ์ฒดํฌํ•ด์ฃผ๊ณ  Finish

  • ํ”„๋กœ์ ํŠธ์— ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜์–ด ์žˆ๋Š” ๋ชจ์Šต

  • Localizable.strings์— ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ๋ฅผ ํ–…์ฃผ๋ฉด ๋˜๋Š”๋ฐ, Key์™€ Value๋กœ ๋‹ค๊ตญ์–ด ์ฒ˜๋ฆฌ๋ฅผ ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค.

  • ๊ทธ๋ฆฌ๊ณ  ๋‹ค๊ตญ์–ดํ™” ํ•œ ๋ฌธ์ž์—ด์„ ์‚ฌ์šฉํ•  ๋• NSLocalizedString ๋ฉ”์†Œ๋“œ๋ฅผ ํ™œ์šฉํ•ด์ฃผ์–ด์•ผ ํ•˜๋Š”๋ฐ, ๋ฒˆ๊ฑฐ๋กœ์šฐ๋‹ˆ extension์„ ํ™œ์šฉํ•˜์—ฌ ๊ฐ„๋‹จํžˆ ์‚ฌ์šฉํ•ด๋ณผ ์ˆ˜ ์žˆ๋‹ค.
test.text = String(format: NSLocalizedString("Test", comment: ""))

// String Extension for Localization
extension String {
    var localized: String {
    	return NSLocalizedString(self, tableName: nil, bundle: Bundle.main, value: "", comment: "")
    }
}
text.text = "Test".localized

์Šคํ† ๋ฆฌ๋ณด๋“œ๋ฅผ ์ฝ”๋“œ๋กœ ๋ง๊ณ  Interface Builder Storyboard ์˜ต์…˜์„ ํ™œ์šฉํ•˜์—ฌ ์Šคํ† ๋ฆฌ๋ณด๋“œ ์ž์ฒด๋ฅผ ์ง€์—ญํ™”ํ•ด์ค„ ์ˆ˜๋„ ์žˆ๋‹ค.

์•ฑ์˜ ์–ธ์–ด๋ฅผ ๋ฐ”๊ฟ€ ๋•Œ๋Š” App Language, App Region ๋‘˜๋‹ค ๋ฐ”๊ฟ”์ฃผ์ž.

์ด๋ฏธ์ง€์˜ ์ง€์—ญํ™”๋Š” Assets์— ์ ‘๊ทผํ•ด์„œ ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆญํ›„ ์šฐ์ธก ์ธ์ŠคํŽ™ํ„ฐ์—์„œ Localization์„ ํ™œ์„ฑํ™” ์‹œ์ผœ์ฃผ๋ฉด ๋œ๋‹ค.

๋‚ ์งœ ์ง€์—ญํ™”

let date = DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .short)
        dateTimeLabel.text = date

ํ†ตํ™” ์ง€์—ญํ™”

func currency(text: Double) -> String? {
    let locale = Locale.current
    let price = text as NSNumber
    let formatter = NumberFormatter()

    formatter.numberStyle = .currency
    formatter.currencyCode = locale.languageCode
    formatter.locale = locale

    return formatter.string(from: price)
}

currencyLabel.text = currency(text: 3000.34)

๋ทฐ์˜ ๋ฐฉํ–ฅ์„ ์ง€์—ญํ™” (๋ฐฉํ–ฅ ๋ฐ”๊ฟ€ ๋•Œ์—๋„ ์œ ์šฉํ•˜๊ฒŒ ์“ฐ๋Š” ๋“ฏ?)

view.semanticContentAttribute = .forceRightToLeft

์—ฌ๋Ÿฌ ๋ฌธ์ž์—ด๋“ค์„ ์ง€์—ญํ™”ํ•  ๋•Œ ๊ตฌ๊ธ€ ์Šคํ”„๋ ˆ๋“œ ์‹œํŠธ๋ฅผ ํ™œ์šฉํ•˜๊ธฐ

  • ๊ตฌ๊ธ€ ์Šคํ”„๋ ˆ๋“œ ์‹œํŠธ๋ฅผ ์ƒˆ๋กœ ์ƒ์„ฑํ•œ ํ›„ ์œ„ ์‚ฌ์ง„๊ณผ ๊ฐ™์ด ๊ตญ๊ฐ€์ฝ”๋“œ์™€ ๋ฒˆ์—ญํ•  ๋ฌธ์žฅ์„ ์ ์œผ๋ฉด ๋œ๋‹ค.
  • ์ขŒ์ธก์— ๊ตญ์ œ์ฝ”๋“œ๋ฅผ ์ ๊ณ  ์šฐ์ธก์— ์•„๋ž˜ ์ฝ”๋“œ๋ฅผ ์ ์œผ๋ฉด ๋ฒˆ์—ญํ•œ ๋ฌธ์žฅ์ด ์ƒ์„ฑ๋œ๋‹ค. (๊ตญ๊ฐ€์ฝ”๋“œ ์ฐธ๊ณ ์‚ฌ์ดํŠธ)
// ์˜ˆ์‹œ
=GOOGLETRANSLATE("Welcome to yagom-academy", "en", A10)

Localization์„ ํ•  ๋•Œ ์œ ์˜ํ•  ์ 

  • ๊ตญ๊ฐ€๋ณ„๋กœ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ์ด ๋‹ค๋ฅด๊ฒŒ ํ•ด์•ผํ•˜๋Š” ์ ๋„ ์ฐธ๊ณ ํ•˜์ž.
  • ๊ตญ๊ฐ€๋ณ„ ๊ธฐ๋Šฅ์ฐจ์ด๋ฅผ ๋‘๋Š” ์ด์œ ๋Š” ํŠน์ • ํ™”๋ฉด์ด๋‚˜ ๊ธฐ๋Šฅ์ด ํŠน์ • ๋‚˜๋ผ์—์„œ๋Š” ์‚ฌ์šฉํ•˜๋ฉด ์•ˆ๋˜๋Š” ๊ฒƒ์ด๋ผ๋˜์ง€, ํŠน์ • ๋‚˜๋ผ์— ํšจ๊ณผ์ ์œผ๋กœ ๋Ÿฐ์นญํ•˜๊ธฐ ์œ„ํ•ด ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์„ ๋„์ž…ํ•œ๋‹ค๋˜์ง€, ๋น„์ฆˆ๋‹ˆ์Šค ์ ์ธ ์ด์œ ๊ฐ€ ๋‹ค์–‘ํ•˜๋‹ค.

top

About

๐Ÿ“ CoreData, Dropbox๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋™๊ธฐํ™” ํ•  ์ˆ˜ ์žˆ๋Š” ๋ฉ”๋ชจ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•œ ์•ฑ

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 100.0%