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