EmojiArt
가 macOS
에서도 작동하도록 바꿔줄거다! 새 프로젝트를 multiplatform app
으로 선언하고, 기존 EmojiArt
의 파일들을 다 복붙해준다. Info.plist
도 업데이트해줘야 하는데 프로젝트 환경설정의 많은 부분이 플랫폼에 따라 상이하기 때문에 iOS
와 MacOS
용이 각각 따로 생성되는 것을 확인할 수 있다. 디폴트로 있던 Document types
, Exported Type Identifiers
, Imported Type Identifiers
를 삭제해주고, 기존 EmojiArt
의 설정을 복붙한다. 이때, MacOS
에서는 Document types
에 Role
항목을 추가해서 값을 Editor
로 추가 설정하는 과정이 필요하다. 그리고 macOS
는 카메라가 없으므로 Privacy - Camera Usage Description
은 iOS
의 Info.plist
에만 추가한다. Camera
와 PhotoLibrary
파일은 iOS
폴더 밑으로 옮겨줄건데, 항상 파일의 빌드 타겟을 확인할 것...!iOS
macOS
macOS
에서는 추가로 entitlement
를 설정해줘야 하는데, info.plist
에서의 privacy settings
의 강화된 버전이라고 생각하면 된다. 우리의 앱이 할 수 있는/없는 것들을 규정하는 보안 설정이다! 아무래도 맥은 운영체제가 더 복잡하고 한 번에 여러명의 유저가 있을 수 있기 때문에 더 신경 써 줄 부분이 많기 때문. 아래와 같은 항목을 추가하고 허용해줘야 나중에 인터넷에서 이미지를 드래그 & 드랍할 수 있다UIImage
는 macOS
에는 없고 iOS
에만 있기 때문에 에러가 뜬다. 따라서 macOS
에서는 완전히 같지는 않지만 비슷한 역할을 하는 NSImage
로 대체해준다. 이때, macOS
용 파일을 따로 선언한 다음, typealias
를 써서 macOS
용 앱 빌드 시에는 UIImage
를 NSImage
로 대체하게 해준다. 물론 UIImage
와 관련된 모든 코드가 해결되는 것은 아니므로 일부는 차후에 또 별도의 대책이 필요하지만 얼추 많은 에러가 해결된다!// macOS
typealias UIImage = NSImage
배경 이미지를 불러온 다음 실제 배경으로 지정해 줄 때 커스텀 View
인 OptionalImage(uiImage:)
를 사용하는데, 내부에서 Image(uiImage:)
를 사용하므로 위와 같은 이유로 문제가 된다.
macOS
에서는 Image(nsImage:)
를 사용하도록 해야되므로 Image extension
을 선언하는데, 여기서 멋진 건 NSImage
를 UIImage
로 typealias
했기 때문에 인자를 UIImage
로 받아도 된다!
// macOS
extension Image {
init(uiImage: UIImage) {
self.init(nsImage: uiImage)
}
}
macOS
는 우리가 PaletteManger
에서 팔레트를 삭제하거나 순서를 변경하는 데 필요한 Editmode
를 지원하지 않는다. 따라서 그냥 PaletteManager
자체를 통채로 iOS
폴더 밑으로 올겨준다.
그러고 나서 macOS
에서는 PaletteManager
가 EmptyView
가 되도록 typealias
하는데, EmptyView
는 컴파일을 위한 꼼수일 뿐 실제로 UI
에 나타나기를 원하는 것은 아니므로 #if ~ #endif
문으로 iOS
일 때만 PaletteManger
버튼이 나타나도록 한다.
// macOS
typealias PaletteMangaer = EmptyView
struct PaletteChooser: View {
@ViewBuilder
var contextMenu: some View {
...
#if os(iOS)
AnimatedActionButton(title: "Manager", systemImage: "slider.vertical.3") {
managing = true
}
#endif
gotoMenu
}
}
macOS
지원하지 않는다. horizontalSizeClass
는 iOS 디바이스에서
너비가 좁을 때는 redo/undo
, paste
, camera
, photo library
버튼들을 contextMenu
안에 넣어버리기 위해 쓴다. macOS
에서는 너비가 좁을 일이 없고, 크기를 항상 키울 수 있으므로 현재 너비가 충분한지 확인할 필요가 없다! 따라서 #if ~ #else ~ #endif
를 사용해서 macOS
인 경우 compact = false
가 되도록 선언한다. struct CompactableIntoContextMenu: ViewModifier {
#if os(iOS)
@Environment(\.horizontalSizeClass) var horizontalSizeClass
var compact: Bool { horizontalSizeClass == .compact }
#else
let compact = false
#endif
func body(content: Content) -> some View {
if compact {
Button {
} label: {
Image(systemName: "ellipsis.circle")
}
.contextMenu {
content
}
} else {
content
}
}
}
macOS
에는 UIDivice
클래스가 없다. UIDevice
는 wrappedInNavigationViewToMakeDismissable(_:)
메서드와 dismissable(_:)
메서드에서 사용하는 데, 이 두 메서드는 아이폰의 경우 화면이 작아 PaletteEditor
외부 영역을 터치하는 방식으로 창을 닫을 수 없기 때문에 PaletteEditor
상단에 close 버튼
을 달아주기 위한 친구들이다. macOS
는 화면 크기가 넓고, 항상 창 크기를 키울 수 있으므로 그냥 iOS 폴더
밑에 iOS
용 파일을 하나 선언해서 거기로 옮겨준다.
그리고 컴파일이 되도록 macOS
용 파일에 아무런 동작 없이 그냥 현재 View
를 리턴하는 wrappedInNavigationViewToMakeDismissable(_:)
메서드를 View extension
에 추가해준다.
// macOS
extension View {
func wrappedInNavigationViewToMakeDismissable(_ dismiss: (() -> Void)?) -> some View {
self
}
}
Camera
와 PhotoLibrary
구조체를 iOS 폴더
로 옮겨줬기 때문에(iOS
에서만 해당 기능을 지원하게 할 거라서) 컴파일이 안된다. macOS
용 파일에서 EmptyView
를 선언한 다음 이걸 Camera
와 PhotoLibrary
로 typealias
해준다. 여기서 주의할 점은 isAvailable 프로퍼티
를 이용해서 EmptyView
가 실제로 UI
에 뜨는 일이 없도록 설정할 수 있어서 이러한 방법을 사용할 수 있다는 것!CantDoItPhotoPicker
가 View
여야 하는 이유는, Camera
와 PhotoLibrary
가 View
이기 때문// macOS
struct CantDoItPhotoPicker: View {
var handlePickedImage: (UIImage?) -> Void
static let isAvailable = false
var body: some View {
EmptyView()
}
}
typealias Camera = CantDoItPhotoPicker
typealias PhotoLibrary = CantDoItPhotoPicker
handlePickedBackgroundImage(_:)
메서드는 UIImage?
를 받아서 Data
타입으로 변환하는데 macOS
에서 사용하는 (즉, UIImage
가 실제로 나타내는)NSImage
에는 jpegData(compressionQuality:)
메서드가 없다. 따라서 일종의 추상화 방식을 채택해서 UIImage extension
으로 imageData
프로퍼티를 선언해서 플랫폼별로 적절하게 구현한다.struct EmojiArtDocumentView: View {
private func handlePickedBackgroundImage(_ image: UIImage?) {
autozoom = true
if let imageData = image?.imageData {
document.setBackground(.imageData(imageData), undoManager: undoManager)
}
backgroundPicker = nil
}
}
iOS
에서는 기존과 같이 jpegData(compressionQuality:)
메서드를 사용하고, macOS
에서는 tiffRepresentation
프로퍼티를 이용해서 TIFF Data
를 얻는다. // iOS
extension UIImage {
var imageData: Data? { jpegData(compressionQuality: 1.0) }
}
// macOS
extension UIImage {
var imageData: Data? { tiffRepresentation }
}
macOS
에는 NSPasteboard
가 있는데 UIPasteboard
와 다르므로 Pasteboard 구조체
를 선언해서 위에서처럼 추상화 해준다. Pasteboard
에서 imageData
를 받아오거나 url
을 받아오므로 이를 구현한다. struct EmojiArtDocumentView: View {
private func pasteBackground() {
autozoom = true
if let imageData = Pasteboard.imageData {
document.setBackground(.imageData(imageData), undoManager: undoManager)
} else if let url = Pasteboard.imageURL {
document.setBackground(.url(url), undoManager: undoManager)
} else {
alertToShow = IdentifiableAlert(
title: "Paste Background",
message: "There is no image currently on the pasteboard."
)
}
}
}
// iOS
struct Pasteboard {
static var imageData: Data? {
UIPasteboard.general.image?.imageData
}
static var imageURL: URL? {
UIPasteboard.general.url?.imageURL
}
}
// macOS
struct Pasteboard {
static var imageData: Data? {
NSPasteboard.general.data(forType: .tiff) ?? NSPasteboard.general.data(forType: .png)
}
static var imageURL: URL? {
// not as? b/c guranteed conversion b/w NSURL and URL
(NSURL(from: NSPasteboard.general) as URL?)?.imageURL
}
}
drop(providers:at:)
메서드는 드랍받은 이미지를 배경화면으로 설정하는데, 이 과정에서 NSItemProvider
와 loadObjects(of:)
메서드를 사용한다. 문제는, NSImage
의 경우 itemProvider
가 존재하지 않는다. 따라서, #if ~ #endif
로 iOS
에서만 UIImage
를 드래그 & 드랍할 수 있게 한다. 이렇게 하더라도 macOS
에서는 url
형태로 이미지를 받으면 되므로, 드래그 앤 드랍을 지원한다. struct EmojiArtDocumentView: View {
private func drop(providers: [NSItemProvider], at location: CGPoint, in geometry: GeometryProxy) -> Bool {
var found = providers.loadObjects(ofType: URL.self) { url in
autozoom = true
document.setBackground(.url(url.imageURL), undoManager: undoManager)
}
#if os(iOS)
if !found {
found = providers.loadObjects(ofType: UIImage.self) { image in
if let data = image.jpegData(compressionQuality: 1.0) {
autozoom = true
document.setBackground(.imageData(data), undoManager: undoManager)
}
}
}
#endif
...
}
}
macOS
에서는 드래그 앤 드랍을 통해 받을 수 있는 타입 중 하나로 String
을 선언할 때, .plainText
가 아니라 .utf8PlainText
를 UTI
로 사용하기 때문이다. 이렇게 선언했을 때 iOS
에서도 작동하므로 그냥 바꿔준다. JSON
도 utf8PlainText
형식struct EmojiArtDocumentView: View {
var documentBody: some View {
GeometryReader { geometry in
ZStack {
// some code
}
.onDrop(of: [.utf8PlainText,.url,.image], isTargeted: nil) { providers, location in
drop(providers: providers, at: location, in: geometry)
}
}
}
}
일단 팔레트 버튼이 찌부됐다...macOS
에서 표준 여백을 팔레트 버튼 아이콘에 적용하기 때문이다...따라서 macOS
인 경우에는 버튼 스타일을 PlainButtonStyle()
이 되도록 하는 View extension
을 선언할 건데 컴파일되도록 iOS
에서도 동일한 이름의 아무것도 하지 않는 extension
을 선언해줘야 한다.
다른 문제 중 하나는 macOS
의 경우 이모지 개수가 많아서 스크롤 해야 하는 경우 제스처로 안되기 때문에 스크롤바를 띄우는데, 이모지 개수가 적은 팔레트는 스크롤바가 없어서 팔레트를 넘길 때 계속 팔레트 창의 크기가 바뀐다. 이 문제는 버튼 상하에 여백을 줘서 해결할 수 있으므로 위에서 extension
에 추가한 메서드를 통해 한번에 해결해줄것...!
// macOS
extension View {
func paletteControlButtonStyle() -> some View {
self.buttonStyle(PlainButtonStyle()).foregroundColor(.accentColor).padding(.vertical)
}
}
// iOS
```swift
extension View {
func paletteControlButtonStyle() -> some View {
self
}
}
macOS
와 iOS
모두에서 View extension
에 이름이 같은 메서드를 선언하고, 전자에서는 좌우 여백을 넣어주고, 후자에서는 아무런 동작도 하지 않게 한다. // macOS
extension View {
func popoverPadding() -> some View {
self.padding(.horizontal)
}
}
// iOS
extension View {
func popoverPadding() -> some View {
self
}
}
macOS
는 상단에 시스템 Edit
메뉴를 제공하므로 redo/undo
버튼이 불필요하다! 따라서 iOS
에서만 나타나도록 바꿔준다. struct EmojiArtDocumentView: View {
var documentBody: some View {
GeometryReader { geometry in
ZStack {
// some code...
}
.compactableToolbar {
#if os(iOS)
if let undoManager = undoManager {
if undoManager.canUndo {
AnimatedActionButton(title: undoManager.undoActionName, systemImage: "arrow.uturn.backward") {
undoManager.undo()
}
}
if undoManager.canRedo {
AnimatedActionButton(title: undoManager.redoActionName, systemImage: "arrow.uturn.forward") {
undoManager.redo()
}
}
}
#endif
}
}
}
}
typealias
, #if #else #endif
, platform-specific file
을 사용할 수 있다는 점을 배웠다. 특히, 각 플랫폼별 파일을 만들어서 한쪽에서는 특정 메서드가 기능하게 하고 다른 쪽에서는 아무런 기능도 하지 않게 하는 방식으로 많은 문제를 해결할 수 있었다! 주의할 점은 컴파일을 위해 한쪽에서는 그냥 EmptyView
를 리턴하게 하는 경우 최대한 EmptyView
자체가 UI
상에 뜨지 않도록 조건을 설정해줘야 하고, 그게 불가능한 경우 다른 해결책을 모색해야 하는게 좀 더 좋은 코드라는 것을 배웠다. Info.plist
를 그냥 파일에서 설정했을 때 이상하게 안 먹혀서 결국 EmojiArt
를 누르면 나오는 셋팅 창에서 설정하느라 시간 엄청 날렸다...아직도 원인을 모르겠음... 완!!!!!!!!강!!!!!!!!!!!! 부끄럽지만 개발 공부를 시작하고 나서 밟은 코스 중에 첫 완강이다...ㅎㅎㅎ 아주 짧게 The Odin Project
의 js
트랙을 밟았었고 cs193p
를 듣기 직전에 100 days of SwiftUI
를 한 10일차인가까지 했었다...중간에 이것저것 다른 공부를 병행하느라 처음 계획한 것보다 많이 밀리긴 했지만 아무튼 과제도 다했고, 강의도 다 듣고, 블로그도 정리했으니까 내 자신 장하다...!
과제...? TODO: figure out a way to drop an NSImage on macOS?
differenet Info.plist for each b/c much of project settings are platform specific
entitlements: security setting that allow ur application to do/not do certain things..similiar to privacy settings in the info.plist
6:30 ) plist 설정 8분
-mac: Csbunldetupe role : Editor
set target for macOS, camera, photolibrary
make wrappedNavigation... return self in MacOS
-? ViewModifiers are a good way to have platform specific behavior
collect things in a platform specific file or have them #if #endif
loadObjects
NSImages are not provided with item providers.. =only url :)
making the drop work
getting rid of the undo button
fixing the PaletteChooser :
- scrollbar adjustment -> need to make size fits the scrollbar all the time
pad popover
mac os entitlement 설정 : com.apple.sercurity.network.client- boolean-true