swift写Mac屏保

突发奇想,想写一个Mac的屏保。但是从网上找了好多,都是用OC写的,并且都是很久以前的。凤毛麟角里找到了一两篇swift的教程,折腾了两天总算完成了。写了一个仿黑客帝国里滚动文字的屏保,虽然丑了点儿,有些晃眼。但谁在乎呢,我又不用,实现才是目的。丑媳妇也要见公婆,先看一下最终效果😂
screensaver

创建一个新项目

选择macOS选项,拉到最底下,最后一个,有一个Screen Saver。这位置,可见很少有人用啊!新建的时候你会发现,并没有勾选swift的选项,没关系,建完之后你就会发现:果然不是Swift的!先运行一下看看,在Products文件夹找到编译后的CoolScreenSaver.saver,这是我自己项目起的名字,反正找到一个.saver的文件就对了。然后右键Open with Extenal Editor,就可以安装了。可以先预览一下一片漆黑的屏保,接下来就可以开始了。

新建一个swift的class,class选择ScreenSaverView。这是Xcode会提示报错,我们需要要把ScreenSaver引入进来,就可以解决了。怎么让程序支持swift呢?在Build Settings -> Building Options里找到Always Embed Swift Standard Libraries,全部设置为YES。然后怎么让Xcode编译swift文件,而不是OC文件呢?好吧,在info.plist中,将Principal Class设置成你的Swift文件中的class,这样就可以使用swift编写了,两个OC文件可以删除了。需要注意的是,当你再编译运行屏保的时候,可能会提示当前版本的macOS不能运行你的屏保,联系管理员blahblah的东西,据说可能是那些swift库没有自动导入,可以通过万能的重启解决!还有一个问题需要注意,你编译并重新安装了新的屏保,系统不会实时刷新,需要关闭之前的偏好设置,重新打开才能预览重新编译之后的结果。嗯,我在这折腾了好久,以为自己写的没有效果,原来是这个原因,妹的!

以上其实就是最主要的了!最难的不是代码,而是这些设置😂

ScreenSaverView的几个方法

我们的这个屏保主要用的这几个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
override init?(frame: NSRect, isPreview: Bool) {
super.init(frame: frame, isPreview: isPreview)
}
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
}
override func startAnimation() {
super.startAnimation()
}
override func stopAnimation() {
super.stopAnimation()
}
override func animateOneFrame() {
}

命名这么明显了,就不多说了。主要工作在animateOneFrame,或者在draw方法里完成。如果是在draw里完成更新,那么需要在animateOneFrame中设置needsDisplay=true。主要的思路就是:

  • 生成一行行竖排文字
  • 随机分布在屏幕各个角落
  • 每一帧重新绘制的时候改变文字的纵坐标
  • 文字出屏之后,重新设置拉回到顶部

主要实现

首先我们先声明一个结构体,用于存储文字坐标等信息。

1
2
3
4
5
6
7
struct TextFieldSet {
var x: CGFloat?
var y: CGFloat?
var h: CGFloat?
var field: NSTextField?
var speed: CGFloat?
}

声明几个变量

1
2
var textFields: [TextFieldSet] = [TextFieldSet]() //存储生成的NSTextField及其相关坐标等信息
let linesOfLetters = 100 //设置要生成的文字最大行数

在初始化方法里随机生成一堆字符串,并生成所需要的一堆TextFieldSet。之后就可以用这一堆TextFieldSet,在屏幕上进行渲染重绘等操作了。

1
2
3
4
5
6
7
8
9
override init?(frame: NSRect, isPreview: Bool) {
super.init(frame: frame, isPreview: isPreview)
for _ in 0..<linesOfLetters {
let str = randomString(Int(SSRandomIntBetween(10, 50)))
self.collectTextField(str) //生成`TextFieldSet`的方法
}
self.animationTimeInterval = 1/60 //设置动画定时器速度
}

里面生成随机字符串的方法:

1
2
3
4
5
6
7
8
9
10
private func randomString(_ length: Int) -> String{
let str = "abcdefghigklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYA0123456789"
var randomStr = ""
for _ in 0..<length{
let index = SSRandomIntBetween(0, Int32(str.count - 1))
randomStr += String(str[str.index(str.startIndex, offsetBy: Int(index))])
}
return randomStr
}

里面获取字符串某个字符位置的方法,着实折腾了一番,基础不够。字符串没法直接使用Int类型的下标来获取某一个字符,只能用String.Index这样一个东西来获取。但我又没找到相关Sting.Index的生成或转译方法。只能通过截取字符串的形式了😂
再来看看生成TextFieldSet的方法,collectTextField:

1
2
3
4
5
6
7
private func collectTextField(_ words: String){
let character = words.characters.map{String($0)}
let ret = character.joined(separator: "\n")
let letter = self.makeTextField(NSString(string: ret)) //生成`NSTextField`的方法
let textField = TextFieldSet(x: letter.x, y: letter.y, h: letter.h, field: letter.field, speed: letter.speed)
self.textFields.append(textField)
}

上面这段主要把文字分割,然后用换行重新拼接起来,这样文字就竖排了。没有找到相关直接竖排的属性或方法,只有先这样了。然后生成NSTextField,并将其自身,以及相关坐标等信息存储到textFields中。接下来看看生成NSTextField的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private func makeTextField(_ character: NSString) -> (field: NSTextField, x: CGFloat, y: CGFloat, h: CGFloat, speed: CGFloat){
let letterX = SSRandomFloatBetween(0, self.bounds.width)
let letterY = SSRandomFloatBetween(0, self.bounds.height)
let rect = NSMakeRect(
letterX,
letterY,
20,
self.bounds.height)
let letter: NSTextField = NSTextField(
frame: rect)
letter.lineBreakMode = .byCharWrapping
letter.isEditable = false
letter.isBordered = false
letter.alignment = .center
letter.backgroundColor = .clear
letter.font = NSFont(name: "Raleway-Medium", size: SSRandomFloatBetween(14, 20))
letter.stringValue = character as String
letter.textColor = .green
letter.stringValue = character as String
letter.alphaValue = SSRandomFloatBetween(0, 1)
return (field: letter, x: letterX, y: letterY, h: self.bounds.height, speed: SSRandomFloatBetween(10, 30))
}

这里面随机生成了NSTextField横纵坐标,同时为了效果更好看一点,稍加美颜,加了随机透明度和随机速度,这样就之后可以看到文字深浅的不同,以及运行速度的不一了。这样,我们所有有用的东西都存在了textFields这个变量里了,接下来就可以用这里面的内容,实现绘制跟更新了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private func updateTextPosition(){
for index in 0..<self.textFields.count {
var text = self.textFields[index]
if let field = text.field{
if text.y! < -text.h!{
text.y = self.bounds.height + text.h!
}
text.y! -= text.speed!
self.textFields[index].y = text.y!
field.frame = NSMakeRect(text.x!, text.y!, 20, text.h!)
self.addSubview(field)
self.setNeedsDisplay(field.frame)
}
}
}

不断的改变每一个NSTextField的纵坐标。需要注意的是,坐标轴的原点竟然是屏幕左下角,前端都是左上角!感觉有些别扭😂
接下来在draw方法或animateOneFrame方法中调用updateTextPosition,就可以不断更新了!速度可以自己调。不知道是电脑卡,还是调的不好,还是需要优化,我觉得好卡😂

最后说一点,如果你想调试屏保,我只找到一个比较简单,但却没什么用处的方法。那就是给项目新建一个target,在这个target的Appdelegate中实例化你的屏保,并将其添加到window.contentView里。这样在屏保程序打个断点,编译新建的target,就可以调试了。但是,我实验的是结果,屏保根本没有在target中绘制,只是init了,所以我只能调试到init方法。
最后,附上GitHub地址:screensaver