인디노트

맥용 (macOS) 메뉴바 앱 (menu bar app) 만들기 - SwiftUI 편 본문

소스 팁/Objective C, Swift, iOS, macOS

맥용 (macOS) 메뉴바 앱 (menu bar app) 만들기 - SwiftUI 편

인디개발자 2021. 4. 9. 08:42

원문참조:

www.anaghsharma.com/blog/macos-menu-bar-app-with-swiftui/

현재 참고 원문 저작자에게 참고 번역에 대한 동의를 얻으려고 연락하고 있습니다. 따라서, 향후 사정에 따라서 본문의 내용이 변경되거나 삭제될 수 있음을 공지 드립니다.


애플의 SwiftUI 를 이용하면 iOS 용과 macOS 용의 앱을 쉽게 만들 수 있습니다. 하지만 메뉴바 앱 같은것을 만들기에는 자료가 너무 부족합니다.

저도 메뉴바 앱을 만들면서 자습해 보는 차원에서 단계별로 요약해서 설명을 하려고 합니다.

우선 메뉴바 앱이란 무엇일까요. 그걸 모르시는 분은 이 페이지를 보러 오지 않았겠죠. 그래도 한번 참고로  설명 드립니다.

OS X 의 기본 상태에서 상단에는 바가 하나 있습니다. 참고로 MS 윈도우즈는 아래쪽에 있는것에 대하여 비교할 수 있습니다. 암튼, MS 와 Apple 은 키보드, 마우스를 사용하는것 빼고는 서로 다르게 하려고 노력하는 모양입니다.

OSX 의 상단 오른쪽에 메뉴바의 일부분

위의 캡춰는 OSX 의 상단에 있는 메뉴바 중에서 오른쪽에 있는 화면입니다.

즉, 우리가 여기서 자습하게 될 내용은 바로 이 곳에 앱의 아이콘 혹은 메뉴를 넣는 것입니다.

 

서론이 길었습니다. 이제, 시작해 봅니다.

 

1. Xcode 에서 새로운 macOS 용 프로젝트 생성

Product Name 은 원하는 프로젝트 이름을 사용하세요.

Organization Identifier 도 원하는 것을 입력하세요.

Interface 는 SwiftUI 로 선택, Life Cycle 는 AppKit App Delegate 선택, 그리고, Language 는 Swift 를 선택 합니다.

Xcode 에서 MyStatusBarApp 프로젝트 생성

 

이렇게 다음과 같은 프로젝트가 하나 만들어집니다.

Xcode 에서 만든 MyStatusNarApp 프로젝트

그냥 이 상태에서 실행을 시켜보면 화면에 다음과 같은 창이 실행됩니다.

Xcode 에서 프로젝트를 만들고 실행한 화면

 

2. 상태바 콘트롤러 (StatusBarController.swift) 라는 클래스 생성

다음과 같이 StatusBarController.swift 라는 swift 소스 파일을 생성하여 내용을 채워 넣습니다.

StatusBarController.swift

위의 캡춰는 이미지로 첨부된 내용이므로 코드의 내용을 긁을 수 없으므로 아래에 있는 코드블럭의 내용 참고 하시면 됩니다.

//
//  StatusBarController.swift
//  MyStatusBarApp
//
//  Created by GYUYOUNG KANG on 2021/04/08.
//

import Foundation
import AppKit

class StatusBarController {
	private var statusBar: NSStatusBar
	private var statusItem: NSStatusItem
	
	init() {
		statusBar = NSStatusBar.init()
		
		// 메뉴바의 길이를 고정값으로 설정합니다.
		statusItem = statusBar.statusItem(withLength: 30.0)
		
		// 컨텐츠 길이에 맞게 길이가 달라집니다.
		// statusItem = statusBar.statusItem(withLength: NSStatusItem.variableLength)
		
		// 상태바에 맞게 컨텐츠가 조정됩니다.
		// statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
		
		if let statusBarButton = statusItem.button {
			statusBarButton.image = #imageLiteral(resourceName: "StatusBarIcon")
			statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0)
			statusBarButton.image?.isTemplate = true
		}
	}
}

 

상태바의 길이에 대하여 몇가지 옵션이 있으니 각각 상황에 알맞게 적용하시면 됩니다.

  1. "StatusBarIcon" 은 메뉴바에 표시할 아이콘 이미지 입니다. 고품질 png 파일을 사용하시면 됩니다.
  2. isTemplate 는 시스템 테마 변경에 따라 아이콘이 모양을 업데이트 하는것에 대한 설정입니다.

 

3. 상태바에 표시될 아이콘 - StatusBarIcon

여기서는 그냥 인터넷에서 적당한 모양의 png 이미지를 복사해서 간단하게 크기 변환을 한 후 사용해 보겠습니다. 이를 위해서 "Image Asset Icon Resizer" 앱을 사용해 보시면 좋을듯 싶습니다.

다음의 주소는 macOS 용 앱스토어의 링크입니다.

무료버전 : apps.apple.com/us/app/image-asset-icon-resizer-lite/id1108313046

 

‎Image Asset Icon Resizer Lite

‎Image Asset Icon Resizer is resizing images for your making any size image assets to app icons, menu icons, tool button icons and launcher images with image naming. With Image Asset Icon Resizer you can resize and efficiently create icons and images for

apps.apple.com

유료버전 : apps.apple.com/us/app/image-asset-icon-resizer-pro/id797183180

 

‎Image Asset Icon Resizer Pro

‎Image Asset Icon Resizer is resizing images for your making any size image assets to app icons, menu icons, tool button icons and launcher images with image naming. With Image Asset Icon Resizer you can resize and efficiently create icons and images for

apps.apple.com

위의 유료버전 앱이 필요하시고 만약 비용을 들이기 어려우시면 여기 블로그 광고 한번 눌러주시고 댓글 혹은 @GYUYOUNGKANG1 로 트윗 혹은 kgy@smartdiskorg 페북 메시지 보내시면 리딤코드 발행해서 보내 드리겠습니다.

 

우선, Xcode 의 Assets 에 StatusBarIcon 을 추가 합니다.

사용할 이미지를 클립보드로 복사한 후 Image Asset Icon Resizer 앱의 클립보드에서 붙혀넣기 버튼을 이용하여 복사한 이미지를 입력합니다.

Image Asset Icon Resizer

[Batch Run] 버튼을 이용하여 생성된 3가지 사이즈의 이미지 파일을 각각 드래그앤드롭으로 Xcode Assets 에 각각 1x, 2x, 3x 에 끌어다 놓으면 됩니다.

Xcode 의 Assets 에 StatusBarIcon 입력

 

4. AppDelegate.swift 코드 적용 - StatusBarIcon

다음으로, 앱이 실행될 때 MenuBarController 클래스 인스턴스를 만들어 초기화 해야 합니다. 그러기 위해서는 AppDelegate.swift 에 다음과 같은 내용을 적용하여 변경해야 합니다.

AppDelegate.swift 의 코드 적용

마찬가지로 내용을 긁어 가시기 편하게 아래 코드 블럭을 첨부 합니다.

//
//  AppDelegate.swift
//  MyStatusBarApp
//
//  Created by GYUYOUNG KANG on 2021/04/08.
//

import Cocoa
import SwiftUI

@main
class AppDelegate: NSObject, NSApplicationDelegate {

	var window: NSWindow!
	
	// 상태바 인스턴스 변수
	var statusBar: StatusBarController?

	func applicationDidFinishLaunching(_ aNotification: Notification) {
		// Create the SwiftUI view that provides the window contents.
		let contentView = ContentView()

		// Create the window and set the content view.
		window = NSWindow(
		    contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
		    styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
		    backing: .buffered, defer: false)
		window.isReleasedWhenClosed = false
		window.center()
		window.setFrameAutosaveName("Main Window")
		window.contentView = NSHostingView(rootView: contentView)
		window.makeKeyAndOrderFront(nil)
		
		// 상태바 인스턴스 초기화
		statusBar = StatusBarController.init()
	}

	func applicationWillTerminate(_ aNotification: Notification) {
		// Insert code here to tear down your application
	}
}

Xcode 프로젝트를 생성할 때 자동으로 만들어진 AppDelegate.swift 코드에 다음의 두가지 내용만을 추가하면 됩니다.

// 상태바 인스턴스 변수
var statusBar: StatusBarController?

// 상태바 인스턴스 초기화
statusBar = StatusBarController.init()

 

이쯤에서 Xcode 의 실행 버튼을 이용하여 앱을 실행시켜 보십시오. OSX 의 상태바에서 앞에서 입력한 StatusBarIcon 의 이미지가 표시되는것이 확인된다면 지금까지의 과정은 잘 된 것입니다.

OSX 상태바 왼쪽에 StatusBarIcon 이 표시됨

 

5. 에이전트 (Agent) 모드로 설정

Xcode 에서 생성한 프로젝트는 기본적으로 Hello, World! 가 표시된 창을 띄우도록 되어 있습니다. 보통 우리가 지금 만들고 있는 상태바 아이콘 앱은 앱을 실행할 때 창을 띄우지 않고 시작을 하게 됩니다. 그 내용을 적용해 보도록 하겠습니다.

Xcode 프로젝트에서 Info.plist 를 찾아서 Application is agent (UIElement) 를 추가하고 값을 YES 로 설정하십시오. 그러면 앱이 Agent 모드로 빌드 됩니다.

Info.plist 에서 Agent 모드 YES 설정

이제 다시 앱을 실행시켜 보시면 Hello. World! 가 표시되는 창은 보이지 않고 상태바에 아이콘만 표시됩니다. 우리가 여기서 만들고자 하는 그런 상태바 앱이 드디어 만들어졌습니다.

 

6. 상태바 아이콘 버튼에 대한 응답

이제, 상태바 아이콘을 클릭하면 해당 이벤트에 응답하여 창을 띄우는 코드를 작성하겠습니다. 이번 포스팅 앞부분에서 만들었던 StatusBarController.swift 의 내용에 관련 코드를 추가 하겠습니다.

StatusBarController.swift 의 코드

마찬가지로 다음의 코드블럭을 참조하시기 바랍니다.

//
//  StatusBarController.swift
//  MyStatusBarApp
//
//  Created by GYUYOUNG KANG on 2021/04/08.
//

import Foundation
import AppKit

class StatusBarController {
	private var statusBar: NSStatusBar
	private var statusItem: NSStatusItem
	
	private var popover: NSPopover
	
	init(_ popover: NSPopover) {
		self.popover = popover
		statusBar = NSStatusBar.init()
		
		// 메뉴바의 길이를 고정값으로 설정합니다.
		statusItem = statusBar.statusItem(withLength: 30.0)
		
		// 컨텐츠 길이에 맞게 길이가 달라집니다.
		// statusItem = statusBar.statusItem(withLength: NSStatusItem.variableLength)
		
		// 상태바에 맞게 컨텐츠가 조정됩니다.
		// statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
		
		if let statusBarButton = statusItem.button {
			statusBarButton.image = #imageLiteral(resourceName: "StatusBarIcon")
			statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0)
			statusBarButton.image?.isTemplate = true
			
			statusBarButton.action = #selector(togglePopover(sender:))
			statusBarButton.target = self
		}
	}
	
	@objc func togglePopover(sender: AnyObject) {
		if(popover.isShown) {
			hidePopover(sender)
		}
		else {
			showPopover(sender)
		}
	}
	
	func showPopover(_ sender: AnyObject) {
		if let statusBarButton = statusItem.button {
			popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
		}
	}
	
	func hidePopover(_ sender: AnyObject) {
		popover.performClose(sender)
	}
}

초기화 루틴 (init) 에 popover 인수를 전달받고 이 popover 를 보이거나 감출수 있는 코드가 추가 되었습니다. 또한, statusBarButton 의 액션 이벤트를 tooglePopover 에서 처리하도록 설정 하는 코드입니다.

 

7. AppDelegate.swift 에 popover 관련 코드 수정

우리는 AppDelegate.swift 의 코드를 일부 수정합니다. StatusBarController 에서 사용할 popover 를 전달하고 일부 필요없는 코드를 제거 합니다.

popover 관련 코드를 추가한 AppDelegate.swift

 

//
//  AppDelegate.swift
//  MyStatusBarApp
//
//  Created by GYUYOUNG KANG on 2021/04/08.
//

import Cocoa
import SwiftUI

@main
class AppDelegate: NSObject, NSApplicationDelegate {
	
	// 상태바 인스턴스 변수
	var statusBar: StatusBarController?
	
	var popover = NSPopover.init()


	func applicationDidFinishLaunching(_ aNotification: Notification) {
		// Create the SwiftUI view that provides the window contents.
		let contentView = ContentView()

		popover.contentSize = NSSize(width: 360, height: 360)
		popover.contentViewController = NSHostingController(rootView: contentView)

		// 상태바 인스턴스 초기화
		statusBar = StatusBarController.init(popover)
	}

	func applicationWillTerminate(_ aNotification: Notification) {
		// Insert code here to tear down your application
	}
}

 

 

8. 상태바 아이콘 클릭하여 popover 형식의 창 띄우기

이제 다시 앱을 실행하고 상태바의 아이콘을 클릭하면... 다음과 같이 팝오버 창이 표시된다면 지금까지의 코드 작업이 완벽한 것입니다.

 

 

-- 추가 기능 넣기 --

9. popover 창의 바깥쪽 클릭시 popover 창 감추기

만약, popover 창의 바깥쪽을 클릭할 때 자동으로 popover 창을 감추기 원한다면 다음의 내용들의 코드작업을 진행하시기 바랍니다.

우리는 EventMonitor.swift 를 새로 생성하여 관련된 코드 작업을 진행하겠습니다.

글로벌 이벤트를 모니터링할 EventMonitor 클래스

//
//  EventMonitor.swift
//  MyStatusBarApp
//
//  Created by GYUYOUNG KANG on 2021/04/09.
//

import Cocoa

class EventMonitor {
	private var monitor: Any?
	private let mask: NSEvent.EventTypeMask
	private let handler: (NSEvent?) -> Void
	
	public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
		self.mask = mask
		self.handler = handler
	}
	
	deinit {
		stop()
	}
	
	public func start() {
		monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) as! NSObject
	}
	
	public func stop() {
		if monitor != nil {
			NSEvent.removeMonitor(monitor!)
			monitor = nil
		}
	}
}

 

이 클래스는 글로벌 이벤트 및 마우스 클릭 또는 제스처와 같은 응용 프로그램 범위를 벗어난 이벤트를 모니터링 합니다. 여기서의 경우는 마우스 왼쪽, 오른쪽 버튼 이벤트를 모니터링 하겠습니다.

또한, 우리는 대략적으로 다음과 같은 코드를 작성하여 StatusBarController.swift 에 적용할 예정입니다.

eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: mouseEventHandler)

 

func mouseEventHandler(_ event: NSEvent?) {
    if(popover.isShown) {
        hidePopover(event!)
    }
}

mouseEventHandler 가 적용된 StatusBarController.swift

//
//  StatusBarController.swift
//  MyStatusBarApp
//
//  Created by GYUYOUNG KANG on 2021/04/08.
//

import Foundation
import AppKit

class StatusBarController {
	private var statusBar: NSStatusBar
	private var statusItem: NSStatusItem
	private var popover: NSPopover
	private var eventMonitor: EventMonitor?
	
	init(_ popover: NSPopover) {
		self.popover = popover
		statusBar = NSStatusBar.init()
		
		// 메뉴바의 길이를 고정값으로 설정합니다.
		statusItem = statusBar.statusItem(withLength: 30.0)
		
		// 컨텐츠 길이에 맞게 길이가 달라집니다.
		// statusItem = statusBar.statusItem(withLength: NSStatusItem.variableLength)
		
		// 상태바에 맞게 컨텐츠가 조정됩니다.
		// statusItem = statusBar.statusItem(withLength: NSStatusItem.squareLength)
		
		if let statusBarButton = statusItem.button {
			statusBarButton.image = #imageLiteral(resourceName: "StatusBarIcon")
			statusBarButton.image?.size = NSSize(width: 18.0, height: 18.0)
			statusBarButton.image?.isTemplate = true
			
			statusBarButton.action = #selector(togglePopover(sender:))
			statusBarButton.target = self
		}
		
		eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown], handler: mouseEventHandler)
	}
	
	func mouseEventHandler(_ event: NSEvent?) {
		if(popover.isShown) {
			hidePopover(event!)
		}
	}
	
	@objc func togglePopover(sender: AnyObject) {
		if(popover.isShown) {
			hidePopover(sender)
		}
		else {
			showPopover(sender)
		}
	}
	
	func showPopover(_ sender: AnyObject) {
		if let statusBarButton = statusItem.button {
			popover.show(relativeTo: statusBarButton.bounds, of: statusBarButton, preferredEdge: NSRectEdge.maxY)
			eventMonitor?.start()
		}
	}
	
	func hidePopover(_ sender: AnyObject) {
		popover.performClose(sender)
		eventMonitor?.stop()
	}
}

앱을 다시 실행시켜 상태바 아이콘을 클릭하여 팝오버 창을 띄운 후 창 바깥쪽을 클릭해보면 팝오버 창이 감춰지는 동작이 구현되면 지금까지의 작업은 완벽히 완료된 것입니다.

 

이상으로 macOS 의 OSX 상태바 앱을 만들어 보았습니다.

참고 되시면 좋겠습니다.

반응형
Comments