了解如何从头开始开发自己的macOS应用程序!

您是否想学习如何开发自己的macOS应用程序,并以此为荣并在您的个人MacBook上使用它? 或者,也许您有激动的热情开始在Mac上进行开发? 然后,您来对地方了! 在这里,我将引导您完成使用最现代的语言之一Swift开发第一个非常macOS应用程序的步骤。

  • 对编程有兴趣
  • 对Swift编程有一些基本的了解(优势)
  • 已安装Xcode 8.3.3
  • 热情打造macOS应用
  • macOS开发的基本概念
  • 如何将Alamofire与macOS应用程序集成以执行网络呼叫
  • 如何创建拖放机制
  • 一些Swift 3语法

我敢肯定,您很高兴知道我们将制造什么好东西! 在本教程中,我们将研究Mac的主要应用程序层Cocoa。 该层负责应用程序的外观和用户动作的响应能力,这也是我们将引入所有可视元素,网络和应用程序逻辑的地方。 我们会将图像上传到uploads.im,因为它提供了供我们使用的开放API。 完成整个教程后,这将是最终产品,

首先将向用户显示一个主屏幕,并显示“在此处拖放图像”的说明。 然后,用户可以将任何 jpg 格式的图像 拖动 到应用程序中,然后该应用程序将显示加载微调框,以通知用户它正在将图像上传到服务器。 服务器 成功 响应后 ,用户将获得一个弹出警报窗口,可以在其中 将URL复制到剪贴板 ,然后用户可以将URL粘贴到任何地方,例如在Internet浏览器上从服务器查看其图像。

聊够了,让我们开始吧!

首先,让我们启动Xcode 9并创建名为PushImage macOS应用项目。 在Application下选择macOSCocoa App

您可以按照我在下一页中输入的设置进行操作,然后为您的项目文件选择一个目录。

现在您已经完成了所有项目的设置,我想借此机会在Xcode 9中重构我最喜欢的一项新功能, 重构! 如果您使用过以前版本的Xcode,则无法重构Swift代码。 让我们试一试。 突出显示ViewController并转到Editor->Refactor 。 让我们将其更改为HomeViewController ,然后单击Rename 。 这将对您的filenameclass nameStoryboard viewcontroller class name进行全局更改, Storyboard viewcontroller class name酷了!

回到我们的HomeViewController类,您将注意到该控制器是NSViewController的子类。 如果您来自开发iOS应用程序,那么您会立刻注意到我们使用NS而不是UI的区别。 那是因为我们使用的是Cocoa (AppKit)而不是Cocoa Touch (UIKit) 。 我认为Apple制定了这两个截然不同的框架,主要是为了将mobileOS背后的技术分开。 drag and drop技术在移动设备上没有用,但在OS上非常有用。 简而言之, UIKitAppKit的精简版。

您可以从此链接下载云图像图标。 拥有资产后,将其拖动到Assets.xcassetsAssets.xcassets重命名为uploadCloud并将资产移动到2x

接下来,让我们移至Main.Storyboard,以创建与用户进行交互的视图。 我们将使用Xcode的漂亮界面生成器来设计主页的外观。 在右下角的容器中,转到第三个项目(即object library并搜索NSImageView ,然后将其拖入HomeViewController视图的中心。

现在,单击您刚刚拖动一次的NSImageView ,然后转到第四项,这是“ Attributes Inspector ,将“ Image设置为uploadCloud 。 因为在最后一步中,我们将其重命名为uploadCloud ,Xcode会自动注册该名称,我们可以通过在此处插入名称直接使用它,而无需指定其文件扩展名。 Xcode的奇迹之一!

让我们也稍微增加一下宽度和高度,以便整体上看起来更好。 单击右侧(实用程序)面板中的第五项。 这是Size Inspector并将其宽度和高度增加到150x150

我们还将需要一个文本来告诉用户他们需要做什么,所以现在继续搜索label ,并将其放在imageView下方。 再次,转到“ Utilities面板,在“ Attributes Inspector ,将“ Title设置为Drag and Drop a .jpg image file here 。 您会看到文本被完全截断,让我们在Size Inspector中将宽度增加到300 ,并将Text Alignment设置为Center-aligned

现在,通过先单击imageView ,然后按住CMD并单击label ,将imageViewlabel都对准窗口的中心。 这应该让您突出显示这两个元素,然后将它们拖动到看到一个十字,如果组件居中,该十字准线将为您准确指示。

我们要添加的倒数第二个UI组件是Indicator 。 继续搜索并将其拖到窗口中,放置在标签中心。

我们需要的最后一个组件是最重要的部件, DragView ! 我们需要一个在我们的应用中充当可放置区域的View 。 因此,遵循与搜索组件相同的习惯,搜索NSView并将其拖入,然后对其进行扩展,以使其填充整个窗口。 Storyboard从下至上或LastInFirstOut概念对图层进行LastInFirstOut 。 因此,现在,我们的视图涵盖了所有其他视图组件。 稍后,我们将视图设置为透明背景,并且不会遮挡其下方的任何视图。

要走的路! 这就是我们需要添加到Storyboard所有组件,现在让我们连接Outlets,这是接口构建器用来连接到其ViewController声明的属性的引用。

使用Storyboard作为活动窗口,我们可以通过激活位于右上方的Assistant Editor来打开一个单独的窗口,其中显示HomeViewController类。

按住ctrl ,单击ImageView并将其拖动到类声明的下面,我们将其称为imageView 。 大! 现在,您的imageView插座已连接到我们类中的属性,我们现在可以通过编程方式调整其属性并使用它。

挑战时间
现在,尝试对viewlabelindicator进行相同的dragView ,分别loadingSpinnerdragViewstaticLabelloadingSpinner 。 您的代码最后应该看起来像这样。

 导入Cocoaclass HomeViewController:NSViewController { 
@IBOutlet弱var imageView:NSImageView!
@IBOutlet弱var staticLabel:NSTextField!
@IBOutlet弱var loadingSpinner:NSProgressIndicator!
@IBOutlet弱变量dragView:NSView!覆盖func viewDidLoad(){
super.viewDidLoad()//加载视图后执行任何其他设置。
}覆盖var namedObject:可以吗? {
didSet {
//更新视图(如果已加载)。
}
}
}

要在窗口中腾出一些空间,让我们回到“标准编辑器”,方法是单击右上角的第一项,我们将移至下一个任务。

创建我们的拖动视图

我们的下一个任务确实是使我们的DragView成为.jpg图像文件的适当“放下”点。 继续, right-click您的根文件夹,然后选择New File 。 选择Cocoa Class并创建一个新的NSView DragViewNSView 。 我们还需要返回到Storyboard ,并将DragView的类设置为DragView 。 然后回到我们的HomeViewController并更改NSView!DragView

  @IBOutlet弱var dragView:DragView! 

如果我们仔细观察一下NSView ,我们发现它实际上自动符合NSDraggingDestination协议,这正是我们需要“下车”空间的条件。

在使用所有可拖动方法之前,我们首先需要register我们的视图。 用以下代码替换视图类中的代码:

 是否需要初始化?(编码器:NSCoder){ 
super.init(编码器:编码器)
registerForDraggedTypes([NSPasteboard.PasteboardType
.fileNameType(forPathExtension:“ .jpg”)])
}

这行代码为文件中注册的任何带有文件扩展名“ .jpg”的拖动项目注册视图。 我们所做的只是注册,我们的视图尚未准备好接受文件。

遵循NSDraggingDestination文档,我们可以得出结论,我们需要这些功能来实现所需的功能:

  1. draggingEntered
  2. draggingUpdated
  3. performDragOperation
 导入Cocoaclass DragView:NSView { 

// 1
私人var fileTypeIsOk =否
私人var acceptedFileExtensions = [“ jpg”]

是否需要初始化?(编码器:NSCoder){
super.init(编码器:编码器)
register(forDraggedTypes:[NSFilenamesPboardType])
}

// 2
覆盖func draggingEntered(_发送者:NSDraggingInfo)-> NSDragOperation {
fileTypeIsOk = checkExtension(拖动:发件人)
返回[]
}

// 3
覆盖func draggingUpdated(_ sender:NSDraggingInfo)-> NSDragOperation {
返回fileTypeIsOk吗? .copy:[]
}

// 4
覆盖func performDragOperation(_ sender:NSDraggingInfo)-> Bool {
警卫队让draggedFileURL = sender.draggedFileURL else {
返回假
}

返回真
}

// 5
fileprivate func checkExtension(拖动:NSDraggingInfo)->布尔{
保护卫队fileExtension = drag.draggedFileURL?.pathExtension?.lowercased()else {
返回假
}

返回acceptedFileExtensions.contains(fileExtension)
}} // 6
扩展名NSDraggingInfo {
var draggedFileURL:NSURL? {
让文件名= draggingPasteboard()。propertyList(forType:NSFilenamesPboardType)为? [串]
让路径=文件名?

返回path.map(NSURL.init)
}
}

1-首先,我们需要创建一个boolean flag调用fileTypeIsOk ,默认为false以帮助我们仅推进图像的正确文件格式。 我们还创建了一个acceptedFileExtensions ,它是string格式的可接受文件格式的数组。

2-当文件首次进入“放置区域”时,将调用draggingEntered函数。 在这里,我们将调用函数checkExtension ,稍后将进行讨论,如果文件类型为.jpg ,则将我们的fileTypeIsOk布尔值设置为true否则将其设置为false

3-在这里实现draggingUpdated函数以获取图像的详细信息。 在这种情况下,如果为fileTypeIsOk ,我们将返回图像的copy ,否则它将返回由[]表示的empty数据。

4 —用户释放鼠标后,将调用performDragOperation函数,稍后我们将利用此函数将URL传递给HomeViewController

5checkExtension是我们的“自制”函数,我们在其中检查drag对象,获取进入的文件的url ,并检查其是否符合我们的acceptedFileExtensions

6 —在这里,我们扩展了NSDraggingInfo ,它实际上是我们在draggingEntereddraggingUpdated看到的所有发件人。 我们在此处添加了一个名为draggedFileURL的变量,以引用图像文件的url。

如果现在运行该应用程序,则应该可以将带有.jpg的图像文件拖入并在光标处看到绿色的+号,但对于其他文件类型则不能。 大! 现在,我们知道我们的应用仅正确接受特定的文件类型,让我们继续在viewcontroller之间建立通信。

创建我们的委托委派模式是Cocoa编程中最常见的模式之一。 这就像创建一个命令中心来广播A所做的事情,因此现在B应该这样做。 简而言之,我们将创建以下这些:

  • 我们的指挥中心(DragViewDelegate)
  • 函数调用didDragFileWith
  • 查看以保存对DragViewDelegate的订户的引用,并调用didDragFileWith
  • 当在View调用DragViewDelegate ,ViewController订阅DragViewDelegate可以执行某些操作。

因此,让我们回到DragView并在import Cocoa下方的类声明顶部输入以下代码:

 通讯协定DragViewDelegate { 
func dragView(didDragFileWith URL:NSURL)
}

我们在此处创建一个委托协议,该协议定义了委托要命令的职责。 在此,唯一的责任是当dragView收到事件调用dragViewDidDragFileWith ,将调用委托,并且订阅者将做出反应。 因此,我们还为订户创建一个变量引用,并将其放在类声明的开头括号之后:

 类DragView:NSView { 

var委托:DragViewDelegate吗?

我们要立即通知HomeViewController放置了正确的文件,因此,在用户释放其拖动事件以及所有检查完成后,委托广播的最佳位置是在performDragOperation中。 因此,继续添加以下代码行:

 覆盖func performDragOperation(_ sender:NSDraggingInfo)-> Bool { 
警卫队让draggedFileURL = sender.draggedFileURL else {
返回假
}

//呼叫代表
如果fileTypeIsOk {
委托?.dragView(didDragFileWith:draggedFileURL)
}

返回真
}

现在让我们回到HomeViewController并扩展我们的类以实现我们的delegate方法。 继续并添加以下代码行:

 扩展HomeViewController:DragViewDelegate { 
func dragView(didDragFileWith URL:NSURL){
打印(URL.absoluteString)
}
}

如果我们现在运行该应用程序,我们将期望看到在控制台中打印出我们的文件URL,请尝试一下!

嗯……当我们将.jpg文件拖入时,什么也没发生,我们错过了什么吗? 是! 我们需要HomeViewController来订阅View的委托。 这是开发人员忘记添加订户的常见遗漏,因此让我们继续进行修复。 在viewDidLoad()添加它:

 覆盖func viewDidLoad(){ 
super.viewDidLoad()
dragView.delegate =自我
}

完成之后,再次运行该应用程序,您将看到打印的URL。 做得好! 您应该走这么远,这在您的背上值得一拍。 我们快到了,我们现在可以将文件推送到服务器了!

Alamofire是一个用Swift编写的功能强大的网络库。 以前称为AFNetworking ,它是迄今为止维护最完善的网络库。

使用迦太基安装Alamofire
随着迦太基图书馆的发展,我对迦太基的热爱已经超过了Cocoapods,因为它更加分散,当一个框架无法构建时,整个项目仍然可以编译。 如果您没有使用迦太基的经验,请查看此链接。

我们首先需要创建一个CartFile.private 。 您可以打开Terminal并导航到项目目录。 {ROOT}/PushImage 。 然后运行vim CartFile.private 。 点击iInsert ,然后复制下面的行。然后点击esc并执行:wqvim是轻量级的编辑器,开发人员通常在诸如TerminalLinux命令提示符环境中使用。

  #CartFile.private 
github的“ Alamofire / Alamofire”〜> 4.5

现在运行carthage update 。 这将克隆依赖项,您应该在Carthage -> Build -> Mac文件夹中获得Alamofire框架。 您应该在最后看到此屏幕:

接下来,导航至Carthage -> Build -> Mac` and drag in Alamofire Framework拖入我们项目的Linked Frameworks and Libraries
您还需要转到“构建阶段”,单击左上角的加号,然后选择“新建复制文件阶段”。 将目的地设置为“ Frameworks”,然后添加Alamofire。

要走的路! 现在,您已经设置了所有网络库!

遵循此处编写的api document 。 我们将利用:

  POST方法发送的uploadURL或图像。 系统自动检测到您已上传。 

就像文档中给出的示例一样执行POST
因此,让我们继续并在dragView函数中添加以下代码行:

 进口可可 
进口Alamofire

 扩展HomeViewController:DragViewDelegate { 
func dragView(didDragFileWith URL:NSURL){
Alamofire.upload(multipartFormData:{(data:MultipartFormData)在
data.append(URL为URL,withName:“ upload”)
},以:“ http://uploads.im/api?format=json”){[弱自我](encodingResult)在
切换encodingResult {
大小写.success(让上载_,_):
upload.responseJSON {在
守护
让dataDict = response.result.value为? NSDictionary,
让data = dataDict [“ data”]为? NSDictionary,
让imgUrl = data [“ img_url”]为? 其他字符串{return}

打印(imgUrl)
}
情况.failure(let encodingError):
打印(encodingError)
}
}
}
}

那是很多代码!!! 别害怕,特别感谢Alamofire,该库为我们提供了一个单一的“上载”函数调用,该函数允许我们使用[multipartFormData,这是一种通常用于上传文件的联网方法)来发布文件。 在这里,我们附加了文件的url,这是文件在字符串中的给定位置,作为带有参数名称uploadURL对象传入。

端点http://uploads.im/api?format=json附加了query parameter format=json ,还可以指定我们想要的格式响应。 当服务器响应upload success完成upload success ,我们将通过解析JSON响应来获取img_url ,并将其打印出来。 试试吧!

糟糕! 当我们在应用程序中进行第一个网络调用时,我们可能还会看到另一个常见的错误消息。 就像这样:

 由于不安全,App Transport Security阻止了明文HTTP(http://)资源加载。 可以通过应用程序的Info.plist文件配置临时异常。 

苹果最近推出了这项新规定,所有应用必须通过https协议进行通信。 由于我们只是在进行实验并将其供个人使用,因此我们可以执行解决方法。

转到项目目录面板上的Info.plist ,右键单击它并选择open作为source code并添加以下代码:

   NSPrincipalClass  
NSApplication
//
NSAppTransportSecurity

NSAllowsArbitraryLoads


//

这将绕过ATS并允许我们与非https协议进行通信。 运行该应用程序,然后尝试上传另一个图像文件! 几秒钟后,您应该会在控制台上看到打印的文件URL! 继续并将其粘贴到浏览器中,以查看现在已从uploads.im服务器加载的本地计算机上的图像!

我们知道我们的Image Loader即将完成,但是我们需要添加一些其他的UX(用户体验)以使其更加完整。

之前我们讨论了加载微调器,所以让我们继续执行加载微调器的逻辑以:

1.在应用启动时隐藏
2.上传时显示动画
3.上传结束或失败时隐藏和停止动画

在我们的viewDidLoad()添加它:

 覆盖func viewDidLoad(){ 
super.viewDidLoad()
dragView.delegate =自我
loadingSpinner.isHidden = true
}

上传时显示和动画
上传完成或失败时隐藏和停止动画

  func dragView(didDragFileWith URL:NSURL){ 
loadingSpinner.isHidden = false
loadingSpinner.startAnimation(self.view)

Alamofire.upload(multipartFormData:{(data:MultipartFormData)在
data.append(URL为URL,withName:“ upload”)
},以:“ http://uploads.im/api?format=json”){[弱自我](encodingResult)在
切换encodingResult {
大小写.success(让上载_,_):
upload.responseJSON {在
守护
让dataDict = response.result.value为? NSDictionary,
让data = dataDict [“ data”]为? NSDictionary,
让imgUrl = data [“ img_url”]为? 其他字符串{return}

self?.loadingSpinner.isHidden = true
self?.loadingSpinner.stopAnimation(self?.view)
}
情况.failure(let encodingError):
打印(encodingError)
}
}
}

将它们添加到我们的dragView函数中。 运行该应用程序并拖入一个图像文件,开始上传后,您应该会看到正在运行的加载微调器!

我们可以看到有时label阻碍了我们的loading spinner ,让我们:

*加载微调器不活动时显示
*激活微调器时隐藏

loadingSpinner.startAnimation(self.view)之后添加staticLabel.isHidden = true ,这显示在loading spinner之后。 和self?.staticLabel.isHidden = false在隐藏self?.loadingSpinner.stopAnimation(self?.view)之后隐藏了loading spinner之后。

再次运行该应用程序,您应该会看到一个漂亮的加载微调器。

我们要添加的最后一个UI组件是Alert Box 。 我们将使用NSAlert,这是一个内置的警报表,具有简单的设置,允许我们进行调整并显示一个不错的弹出警报框。

继续并添加此功能,它将显示我们的NSAlert弹出框:

  fileprivate func showSuccessAlert(url:String){ 
让警报= NSAlert()
alert.messageText =网址
alert.alertStyle = .informational
alert.addButton(withTitle:“复制到剪贴板”)
让响应= alert.runModal()
如果响应== NSAlertFirstButtonReturn {
NSPasteboard.general()。clearContents()
NSPasteboard.general()。setString(url,forType:NSPasteboardTypeString)
}
}

然后,继续并在self?.staticLabel.isHidden = false之后添加以下代码:

 自我?.showSuccessAlert(URL:imgUrl) 

现在运行该应用程序并将图像拖入其中,您应该会看到一个漂亮的带有按钮的本机弹出警报框,单击该按钮时,它将URL复制到剪贴板,您可以将其粘贴到任何地方以供个人使用!

那么我们取得了什么成就?

  • 我们学习了如何使用Cocoa从零开始完全构建macOS应用。
  • 我们学习了如何使用Xcode IB设计UI元素。
  • 我们学习了如何创建自定义视图类
  • 我们学习了如何设计委托模式
  • 我们学习了如何使用Carthage安装外部库。
  • 我们学习了如何使用Alamofire上传图片。
  • 我们学习了如何使用uploads.im的开源API。
  • 我们学习了如何使用NSAlert

通常,这就是创建macOS应用程序的方式,我没有讲过很多内容,这取决于您进一步探索并制作有意义和有用的产品,供世界各地的人们使用!

如果您对本教程有任何疑问,请在下面留下您的评论,并让我知道。

对于示例项目,您可以在GitHub上下载完整的源代码。