John Ninja

this is my test blog


  • 首页

  • 归档

  • 标签

swift写Mac屏保

发表于 2017-09-29

突发奇想,想写一个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

乞丐版swift placeholder动画

发表于 2017-08-11

在swift群里发现有人在找这种动画。感觉这种应该可以自己写,不用找别人的吧?于是自己就想试试能不能成功,毕竟菜鸟。下面是动画效果,大家应该都见过的。

placeholder

没错,就是这个简单的效果!由于分辨率的问题,输入框的边框没显示全😂。自己找了一点儿资料,发现不是很复杂。但是自己对于iOS接触的太少,里面的API什么的都不熟悉,属于摸着石头过河。基本的实现方式就是自定义UITextField这个控件。

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
27
28
29
30
class LabelInput: UITextField {
var placeholderLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))
var animationDuration: TimeInterval = 0.35
override func draw(_ rect: CGRect) {
super.draw(rect)
self.addTarget(self, action: #selector(didEditEnd), for: .editingDidEnd)
}
override func drawPlaceholder(in rect: CGRect) {
super.drawPlaceholder(in: rect)
placeholderLabel = UILabel(frame: CGRect(x: rect.origin.x, y: rect.origin.y, width: rect.size.width, height: rect.size.height))
placeholderLabel.text = placeholder
self.placeholder = nil
placeholderLabel.font = UIFont(name: self.font!.fontName, size: self.font!.pointSize)
self.addSubview(placeholderLabel)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
UIView.animate(withDuration: animationDuration, animations: {
self.placeholderLabel.center = CGPoint(x: self.placeholderLabel.center.x, y: -20)
})
}
func didEditEnd(){
if text?.characters.count == 0 {
UIView.animate(withDuration: animationDuration, animations: {
self.placeholderLabel.center = CGPoint(x: self.placeholderLabel.center.x, y: self.frame.size.height/2)
})
}
}
}

这真真是一个乞丐版了。基本思路就是重写drawPlaceholder这个方法。新建一个UILabel替换掉原来真正的placeholder,然后接下来动画改变的其实是这个label。点击输入框的时候改变label的坐标,输入结束的时候判断一下输入框内容,如果是空的就让label回到原来位置。

这个乞丐版只是改了一下y坐标,颜色什么都没关注,动画也没有调试,没有精确计算,只是一个思路而已。写前端的时候喜欢自己写这些控件之类的,我感觉有些简单的东西,自己写比到网上现找别人的要简单。多动手吧😂

swift页面跳转

发表于 2017-07-31

今天搞通了swift页面跳转。如果对比前端或者说web应用,其实就是“路由”,像react应用,可以通过react-router来管理路由,vue可以通过vue-router来管理路由类似,swift中可以用UINavigationController来管理“路由”,这里应该叫“导航”吧。iOS中有两种不同形式的跳转,一种是有逻辑层级关系的跳转,一种是临时页面的跳转。

具有逻辑层级关系的页面跳转

比如网上购物,下单之后要跳到订单页面,再跳到支付页面等等,这种跳转是有一定逻辑,或者说先后顺序的。这种跳转需要用NavigationViewController进行跳转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//创建一个按钮,用来跳转页面
let btn = UIButton(frame: CGRect(x: 100, y: 80, width: 100, height: 45 ))
btn.setTitle("Next", for: .normal)
btn.backgroundColor = .blue
btn.addTarget(self, action: #selector(gotoNext), for: .touchUpInside)
self.view.addSubview(btn)
}
func gotoNext() {
let anotherView = HelloController() //要跳转到的页面
self.navigationController?.pushViewController(anotherView, animated: true)
}
}

上面的代码,使用navigationController下的pushViewController方法进行跳转。同时新建一个Cocoa Touch Class文件,然后选择subclass为UIViewController,文件名是上面代码用到的HelloController,这就是要跳转到的第二个页面。我们在这个页面放一个返回的按钮,跳回到原来的页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class HelloController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton()
button.frame = CGRect(x: 50, y: 80, width: 100, height: 45)
button.setTitle("Back", for: .normal)
button.backgroundColor = UIColor.cyan
button.addTarget(self, action: #selector(back), for: .touchUpInside)
view.addSubview(button)
view.backgroundColor = UIColor.white //设置一下北京色,不然是黑的😂
print("second page")
}
func back() {
// 返回上一页
self.navigationController?.popViewController(animated: true)
}
}

点击Back按钮,返回上一页。使用navigationController下的popViewController方法实现返回。但是如果这时编译页面,在模拟器中点击Next按钮,发现并没有任何事情发生😅这块找了半天才找到解决办法,最后发现,是没有实例化UINavigationController,其实看到navigationController是一个可选类型就可以猜到一二。接下来,该在什么地方实例化这个UINavigationController呢?如果你直接在页面实例化,发现一样不管用。类比一下react里的路由(react-router 4.0之前的版本),就会发现,导航其实是一个全局的东西,来管理所有的界面堆栈,而不是存在于某一个页面内。如果放入某一个页面内,每次加载都是实例化的一个新UINavigationController,它在其他页面是没法被拿到的。如这个例子中,如果在第一页实例化一个导航控制器,在第二页中是拿不到的。所以,要解决这个问题,就要实例化一个全局的UINavigationController。我们可以在AppDelegate.swift中实例化这个全局导航器。在application方法中,添加如下代码:

1
2
3
4
5
6
let mainPage = ViewController()
let navRoot = UINavigationController(rootViewController: mainPage)
window = UIWindow(frame: UIScreen.main.bounds)
window?.backgroundColor = UIColor.white
window?.rootViewController = navRoot

即通过给window的rootViewController赋值一个UINavigationController实例,就可以在所有页面使用navigationController了。这里其实是重写了应用启动后的默认行为,所以需要在实例化UINavigationController时指定一下根试图。同时,把window大小设置成了满屏。然后给个背景色,否则是黑的😓。这样就可以了,其他页面直接使用pushViewController和popViewController就可以了。

临时页面的跳转

临时页面跳转,不知道有没有一个专业的名称。我觉得可以看作是一个弹出页,比如点击一个表单控件,可以弹出一个相应的编辑页。这个是属于该页面的行为,所以不需要全局使用,视图自带。代码还是用上面的,不过跳转方法改一下就可以了。跳转到下一页,可以用self.present(anotherView, animated: true, completion: nil),这样页面会从底部弹出。收回页面,调用self.dismiss(animated: true, completion: nil)。这里灰常简单,就认识俩方法而已。

over!

swift闭包

发表于 2017-07-26

闭包

闭包表达式的形式

1
2
3
{ (parameters) -> returnType in
statements
}

关键字in表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始

尾随闭包

当闭包当作最后一个参数传入另一个函数时,可以用尾随闭包来增强可读性。

1
2
3
func closureTest(closure: () -> Void){
closure
}

调用时:

1
2
3
closureTest(){
print("这里是尾随闭包")
}

跟在函数调用后,以大括号包裹的函数体,就是尾随闭包。

逃逸闭包

当一个闭包作为参数传入另一个函数,但是这个闭包在函数执行之后才被执行,这类闭包即是逃逸闭包。逃逸闭包通过在参数名之前标注@escape来表示。

1
2
3
4
var escapingFuncs: [() -> Void] = []
func escapingTest(closure: @escaping () -> Void){
escapingFuncs.append(closure)
}

如上形式就是一个逃逸闭包,如果不加@escaping标注,会得到一个编译错误。

逃逸闭包如果用到self必须显示地声明,如下:

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
var escapingFuncs: [() -> Void] = []
func escapingFunc(closure: @escaping () -> Void){
escapingFuncs.append(closure)
}
func withoutEscapingFunc(closure: () -> Void){
closure()
}
class EscapeTest{
var x = 10
func changeX() {
escapingFunc(){
self.x = 100
}
withoutEscapingFunc(){
x = 200
}
}
}
let instance = EscapeTest()
instance.changeX()
print(instance.x) // 200
escapingFuncs.first?()
print(instance.x) // 100

自动闭包

自动闭包是最让我费解的一种。官网一直强调它的延迟执行特点。然而所有的函数都是延迟执行,不去调用函数,只去声明函数,当然不会执行函数体内的语句。这也能算一个特性?我只能理解为,相对于其它闭包来说,这是一个特性😂
官网的例子:

1
2
3
4
5
6
7
8
9
10
11
12
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine)
// 打印"5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印"5"
print("Now serving \(customerProvider)")
// 打印 "Now serving Chris!"
print(customersInLine.count)
// 打印"4"

emmm……还是没太明白,这跟直接声明一个函数有什么区别?😂

作为参数传递给函数,参数名前加一个@autoclosure进行标注:

1
2
3
4
5
func serve(customer customerProvider: @autoclosure () -> String){
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

如果没加@autoclosure标注时,传参的地方需要用大括号包裹serve(customer: { customersInLine.remove(at: 0) })。emm….这样对比一下,倒是可以理解延迟加载的意思了。不过感觉还是很鸡肋😂

最后,如果使一个自动闭包也可以逃逸,那就需要两个标注都加上

1
2
3
func serve(customer customerProvider: @autoclosure @escaping() -> String){
`...`
}

简单的记录一下,over!

正则中的问号

发表于 2017-06-07 | 分类于 正则

几乎每一个框架中都或多或少有正则表达式的出现,一个正则表达式,可以节省许多各种冗长的匹配代码。今天看到某一框架,突然里面的正则不是很复杂,于是想研究一下。发现看不懂!尤其是对里面的“?”。于是赶紧补充了一下知识,以此记之!

问号在正则中有许多用法,这里我就把我找到几个用法罗列一下,主要参考的:

“?”匹配前面的字符,0次或1次,相当于{0,1}

比如/e?le?/会匹配“angular”中的“l”,会匹配“angel”中的“el”,会匹配“angel”中的“el”。对字母“e”匹配0或1次。

阅读全文 »

Mac配置github

发表于 2017-06-07 | 分类于 git

本人苦逼前端一枚,在一个苦逼的城市里的一个苦逼公司里苦逼的搬砖。由于种种原因,前端并不被重视,连提交到代码库的机会都没有😂。并且团队都用svn管理代码,我只有自己研究了。第一次在github上写博客,这篇毫无技术含量的文章(咳咳,即便如此,也是参考网上的),权当练手了。

要在Mac上面使用Github,大致就简单的两步。一、生成ssh的公钥;二、把生成的公钥添加到Github中。就是这点儿事而已,也值得记?😂

一、生成ssh公钥

利用以下命令生成公钥

$ ssh-keygen -t rsa -C xxx@email.com 
Generating public/private rsa key pair.
    Enter file in which to save the key (/Users/username/.ssh/id_rsa): 
    Created directory '/Users/username/.ssh'.
    Enter passphrase (empty for no passphrase): 
    Enter same passphrase again: 
    Your identification has been saved in /Users/username/.ssh/id_rsa.
    Your public key has been saved in /Users/username/.ssh/id_rsa.pub.
    The key fingerprint is:
    18:16:11:c9:01:6c:48:09:7f:27:c6:43:0d:7f:3f:84 xxxxx@gmail.com
    The key's randomart image is:
    +--[ RSA 2048]----+
    |.o.++===         |
    |.ooo.+. .       |
    |  ..* = E .      |
    |   o = + o       |
    |      . S o      |
    |           .     |
    |                 |
    |                 |
    |                 |
   +-----------------+

以上邮箱为你在Github注册的邮箱,生成密钥过程中会提示你选择密钥的存放位置。如果你不做修改,会默认存把密钥放在当前用户的.ssh目录下。之后会提示输入密码,密码可以为空。

二、把生成的公钥添加到Github

登录Github,选择Account Settings –> SSH Keys添加新的ssh
其中title为这个ssh key的名字,可以随便起。不过你的ssh key可能不只这一个,为了方便管理,最好起一个有意义的名字。

之后在key文本域中添加刚刚生成的公钥。进入你之前保存密钥的目录,将名为id_rsa.pub的公钥里面的内容复制到key里,保存即可。

之后可以通过以下命令检测是否配置成功。

1
$ ssh -T git@github.com

配置成功后会出现以下提示

1
Hi MiracleHe! You\'ve successfully authenticated, but GitHub does not provide shell access.

三、测试提交代码

之后正好用这篇文章测试一下,git push之后,报了以下错误:

1
2
fatal: remote error:
You can\'t push to git://github.com/johnninja/johnninja.git

查阅过后,发现这是由于之前我是通过git@github.com/xxx/xxx.git方法clone过来的代码,而git协议不支持此方法push。

可以通过以下方法来解决这个问题:

1
2
$ git remote rm origin
$ git remote add origin git@github.com:username/xxx.git

这样就大功告成了!

Johnninja

Johnninja

nothing's here

6 日志
2 分类
© 2017 Johnninja
由 Hexo 强力驱动
主题 - NexT.Mist