开源项目-WaterMark

前言

这里引用应用描述里的一句话:

本项目代码全部开源,只删除了LeanCloud相关的id和key,如果我以后不小心把key和id push上去了各位小伙伴一定要提醒我哈…

这个项目是业余时间瞎敲出来的…很多代码都是自己一边尝试一边敲出来的…所以大家可以挑重点看

我准备把WaterMark 和以后其他的开源项目写成一个系列的博客,每次更新版本都会把遇到的难点和坑点总结出来..喜欢的话大家可以收藏,关注支持一下

希望本篇博客能帮助菜鸟了解iOS项目开发中的常识

欢迎大家一起讨论正确的开发姿势

跪求大神带我飞!

(在下英文渣,请各位老爷观看时利用脑内runtime 把不对的单词替换成对的单词…哈哈哈,我会好好背单词的!)

项目地址:https://github.com/Lafree317/WaterLabel

AppStroe:https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewSoftware?id=1148289486&mt=8

回归正题

目前功能:

  • 原尺寸/缩略图 添加水印
  • 水印缓存
  • 水印编辑

架构

项目文件结构

项目中现在集成的第三方开源库有:

  • Pod
    • LeanCloud Swift-alpha版(坑): 用于反馈数据的云存储
    • MBProgressHUD 1.0 :提示控件 每个项目必备
    • RxSwift和RxCocoa: 现在代码中还没有用到,到时候写登陆的时候准备这个写响应式
    • SnapKit: Swift版的Messary 炒鸡好用
  • Vendors
    • ShowString 我司小哥改装的一个提示框,优点在于提示的时候还可以对页面进行UI交互
    • TZImagePickerController 一个1000+star图片选择库,这个开发者很热心发issues很快就会回复你并解决问题
    • IGLDropDownMenu 一个下拉抽屉式动画,使用起来非常方便
    • ZEViewKit 我自己封装的一些小控件,准备再赞一些一起上传到Pod中
    • Scenes里有一个ZEVC是准备以后所有开源项目中公用的反馈和登陆界面

项目架构采用最基本的MVC

因为不是公司项目喜欢自己瞎搞一些东西所以一些被我改畸形了
这里详细说一下:

我发现Swfit的Extension太过强大于是将Model层和View层都用extension来代替了,如feedBackController的代码就被我改成:

  • Controller

    1
    2
    3
    4
    5
    6
    7
    8
    class ZEFeedBackContoller: UIViewController,UITextFieldDelegate,UIScrollViewDelegate {
    // 各种属性

    override func viewDidLoad() {
    super.viewDidLoad()

    setUI()
    }
  • Model

    1
    2
    3
    4
    5
    extension ZEFeedBackContoller {
    // model方法
    func sendFeedBack(){
    }
    }
  • View

    1
    2
    3
    4
    extension ZEFeedBackContoller {
    // 添加UI
    func setUI(){
    }

首先强调一点…这是我自己胡乱尝试敲出来的…没有看过类似代码..所以不推荐在正常项目中使用,只是讲一下自己的思路希望能帮助到你

这样做的好处我觉得有几点

  • 代码都为C的代码,不存在跨类调用方法和取值(高内聚?)
  • 上下两个控制器传值调用的时候也更方便一些(低耦合?)
  • C代码很少,只把主要的方法C中,如果想扩展相应功能或修改bug可以直接找到位置去进行操作

我觉得不好的地方

  • 代码较为混乱,不适合多人开发?
  • 具体功能重用性差,不利于其他项目使用

我一直自己摸索学习Swift,还没有找到它正确的开发姿势,希望大家能教我正确的开发姿势

项目中还有一小部分面向协议

如给UIView添加一个滑动手势,这里应该如果优化一下应该 extenion到具体类 而不是直接给UIView添加..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protocol ViewGestureRecognizer{


}
extension UIView:ViewGestureRecognizer{
func addPan(){
self.userInteractionEnabled = true
let pan = UIPanGestureRecognizer(target: self, action: #selector(pan(_:)))
self.addGestureRecognizer(pan)
}
func pan(pan:UIPanGestureRecognizer){
let point = pan.translationInView(self)
self.transform = CGAffineTransformTranslate(self.transform, point.x, point.y)
pan.setTranslation(.zero, inView: self)
}
}

一些细节

水印的绘制是由EditModel这个类实现的

原理就是利用UIGraphics 的 UIImage context 先按照图片尺寸绘制出背景图片,然后把label一个一个的绘制到图片上 最后导出一张绘制好的图片保存到相册中

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
31
32
33
34
35
36
/**
添加水印
*/
func save(){
guard let image = imageView.image else{
return
}
weak var weakSelf = self
guard let wself = weakSelf else{
return
}
ZEHud.sharedInstance.showHud()
dispatch_async(dispatch_queue_create("addLabel",nil)) {
UIGraphicsBeginImageContext(image.size)// 开始绘制
image.drawInRect(CGRect(origin: CGPoint.zero, size: image.size))
for label in wself.labelArr { // 添加多个水印
let rect = wself.imageView.convertRect(label.frame, fromView: nil)
let reScale = 1/wself.imageView.scale
let labelRect = CGRectMake((rect.origin.x)*reScale, (rect.origin.y*reScale), rect.size.width*reScale, rect.height*reScale)
label.model.text.drawInRect(labelRect, withAttributes:label.model.getAttributes(1/wself.imageView.scale))
}
let imageA = UIGraphicsGetImageFromCurrentImageContext()// 获取图片
UIGraphicsEndImageContext()// 结束绘制
UIImageWriteToSavedPhotosAlbum(imageA, self, nil, nil)// 保存
dispatch_async(dispatch_get_main_queue(), {
ZEHud.sharedInstance.hideHud()
ShowString.sharedManager().showStringView("保存成功")
wself.imageView.image = imageA
wself.assets.removeAtIndex(wself.index)
wself.changeImage(wself.index)
if weakSelf!.assets.count == 0 {
weakSelf?.performSelector(#selector(weakSelf?.nodataPop), withObject: nil, afterDelay: 0.75)
}
})
}
}

项目中水印是由WaterMark这个类来实现的,因为偷懒所以直接继承自UILabel(获取尺寸的时候回方便一些),然后在Label下面添加了一个TextField,来进行文字编辑.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 通过传入的bool进行label的文字变换
func changeEidtType(type:Bool){
if type == true {
self.textField.font = self.font
self.textField.text = self.text
self.textField.becomeFirstResponder()
}else{
textField.endEditing(true)
let dic = model.getAttributes(1)
let att = NSAttributedString(string: self.textField.text!, attributes: dic)
self.attributedText = att
}
textField.hidden = !type
viewChange()
}

每次编辑水印的步骤我设计成 长按Label -> 弹出EditView -> 每一次操作都做相应的处理(缓存,改变Label的状态)

label的样式是由NSMutableAttributedString这个类来实现的,这个类可以直接用在绘制里

难点

Label和ImageView的比例计算

我给ImageView的class声明了一个scale属性,调用这个属性就会计算出比例的变量

绘制Label的时候就按照1/scale的比例逆推回原大小进行绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class EditImageView: UIImageView {
var scale:CGFloat {
get{
return frame.width / image!.size.width
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
func setNewImage(newImage:UIImage){
image = newImage
self.frame.size.width = screenWidth
frame = CGRectMake(0, 0, screenWidth, newImage.size.height * scale)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

水印的本地缓存

缓存格式:

颜色缓存是一个坑…如果用系统自带的颜色直接缓存的话会有问题,因为width black gary 和其他颜色的属性是不一样的,这三类颜色只有黑色和透明度而其他颜色都是RGB 不能进行统一处理

于是我就自己归档了一个颜色数组(查系统的色值还挺麻烦的..)

1
2
3
4
5
6
7
let titleColorArr:Array<[String:[String:CGFloat]]> = [
["黑色":["red":0,"green":0,"blue":0,"alpha":1]],
["灰色":["red":102,"green":102,"blue":102,"alpha":1]],
["白色":["red":255,"green":255,"blue":255,"alpha":1]],
["透明":["red":255,"green":255,"blue":255,"alpha":0]],
...
]

然后声明了两个方法,一个是字典转颜色,一个是颜色转字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class ColorFile {
static func colorToDic(color:UIColor) -> [String:CGFloat] {
let components = CGColorGetComponents(color.CGColor)
let r = components[0]
let g = components[1]
let b = components[2]
let a = components[3]
return ["red":r,"green":g,"blue":b,"alpha":a]
}
static func dicToColor(dic:[String:CGFloat]) -> UIColor {
let r = dic["red"]!
let g = dic["green"]!
let b = dic["blue"]!
let a = dic["alpha"]!
return UIColor(red: r, green: g, blue: b, alpha:a)
}
}

写入缓存的时机是LabelModel每一个属性改变的时候

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var italic:Bool = false{
willSet{
self.italic = newValue
setUD(self.italic, key:italicUDK)
}
}
var underLine:Bool = false{
willSet{
self.underLine = newValue
setUD(self.underLine, key:underLineUDK)
}
}

func setUD(value:AnyObject?,key:String){
NSUserDefaults.standardUserDefaults().setObject(value, forKey: key)
NSUserDefaults.standardUserDefaults().synchronize()// 同步数据
}
func getUD(key:String) -> AnyObject? {
return NSUserDefaults.standardUserDefaults().objectForKey(key)
}

labelModel初始化的时候就会取缓存,如果没取到就会给一个默认值

1
2
3
4
5
6
7
init(){
if let text = getUD(textUDK) as? String {
self.text = text
}else{
text = "这是第一个水印"
}
}

当ImageView被拖拽放大时 相对 Label的笔记变化

这个还未解决,在上线前已经禁止掉了ImageView的手势

每次imageView大小变化的时候用Label按照scale逆推回去就会发现比例不对,不能按照原比例绘制,求大神帮忙..

欢迎来到吐槽时间…

LeanCloud-Swift-SDK就是一个坑啊…代码难懂不说居然只支持iOS9.1以上版本…相信这一条就让99%的项目不准备引用了吧…

然后他们居然还在info.plist里面的short boundle version里面写英文!握草我头一次遇见打包时候这个Error类型,为此我还特意记了一条笔记….

推荐

推荐一个AppIcon快速生成插件,可以用Package Manager直接下载或者直接去git->https://github.com/kaphacius/IconMaker

使用方法:打开Assets->右键选中AppIcon->选中Make An App Icon -> 选择图片 -> 成功!

现在审核时各个型号的手机可以共用一套简介图片了(可能不是最近才改的,一直是另外一个小哥负责打包的 @辛勤的另外一个小哥)

结尾语

现在项目刚刚提交了审核,审核通过了再把appstore链接贴上…

我都是想起哪里说哪里…可能有些难懂,如果有不懂的地方可以在评论里和我说我会及时修改文章的

睡觉明早起来再改错别字…