Swift封装 - 计算器开发

image.png
前言:

师弟要毕业设计,就敲了swift版的计算器给他参考下。现在把代码放上来,通过这个计算器,可以学习简单的封装:将逻辑与界面分离并提供接口的编程方式,这也是我们学习面向对象的必要点。

基于 xcode 9.0 swift4.0

一、先引用SnapKit框架
SnapKit自己看git引入
利用其来约束组件

二、新建一个继承UIButton类的类文件,命名为DWFuncButton,对其设置字体、颜色、风格代码如下:

class DWFuncButton: UIButton {

    init() {
        super.init(frame: CGRect.zero)
        //为按钮添加边框
        self.layer.borderWidth = 0.5;
        self.layer.borderColor = UIColor(red: 219/255.0, green: 219/255.0, blue: 219/255.0, alpha: 1).cgColor
        //设置字体与字体颜色
        self.setTitleColor(UIColor.orange, for: .normal)
        self.titleLabel?.font = UIFont.systemFont(ofSize: 25)
        self.setTitleColor(UIColor.black, for: .highlighted)
        
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

三、创建一个继承UIView的类,命名为DWBoard,将其用作计算器的操作面板
首先引入SnapKit框架
import SnapKit
先创建一个数组属性,存放操作面板上的所有功能按钮标题

var dataArray = ["0", ".", "%", "="
                , "1", "2", "3", "+"
                , "4", "5", "6", "-"
                , "7", "8", "9", "*"
                 , "AC", "DEL", "^", "/"]

重写父类的构造方法,在其中进行界面的加载操作:

override init(frame: CGRect) {
        super.init(frame: frame)
        setupUI()
    }
//对界面进行布局
func setupUI() {
    //创建一个变量 用于保存当前布局按钮的上一个按钮
    var frontBtn: DWFuncButton!
    //进行功能按钮的循环创建
    for index in 0..<20 {
        //创建一个功能按钮
        let btn = DWFuncButton()
        self.addSubview(btn)
        //约束
        btn.snp.makeConstraints({ (make) in
            //当按钮每一行的第一个时,将其靠左侧摆放
            if index%4 == 0 {
                make.left.equalTo(0)
            }else { //否则将按钮的左边考上一个右侧进行摆放
                make.left.equalTo(frontBtn.snp.right)
            }
            //当按钮为第一行,将其靠父视图底部摆放
            if index/4 == 0 {
                make.bottom.equalTo(0)
            }else if index%4 == 0 { //当按钮不在第一行且为每行的第一个时,将其底部与上一个按钮的顶部对齐
                make.bottom.equalTo(frontBtn.snp.top)
                //否则将其底部与上一个按钮底部对齐整
            }else {
                make.bottom.equalTo(frontBtn.snp.bottom)
            }
            //约束宽度为父视图宽度的0.25倍
            make.width.equalTo(btn.superview!.snp.width).multipliedBy(0.25)
            //约束高度为父视图宽度的0.2倍
            make.height.equalTo(btn.superview!.snp.height).multipliedBy(0.2)
        })
        
        //设置tag值
        btn.tag = index + 100
        //添加点击事件
        btn.addTarget(self, action: #selector(btnClick(_:)), for: .touchUpInside)
        //设置标题
        btn.setTitle(dataArray[index], for: .normal)
        //对上一个按钮更新保存
        frontBtn = btn
    }
    
}

上面就构建了一个简单的键盘界面,约束代码大家可以看一下,排版为5行4列,布局顺序为从下向上、从左向右依次布局

创建上述代码的点击方法

@objc func btnClick(_ button:DWFuncButton) {
        print(button.currentTitle as Any)
    }

用户在操作面板上进行输入操作,在计算器的显示屏上还需要显示输入的内容,同时,显示屏还兼有计算结果的功能。
首先在DWCalculator工程上新建一个名为DWScreen的类文件,继承自UIView,作为计算器的显示器控件。显示屏分成两部分,一部分用于计算结果,一部分用于显示用户输入的计算过程,所以用两个UILabel来处理。

class DWScreen: UIView {

    var inputLabel:UILabel?
    //用于显示历史记录信息
    var historyLabel:UILabel?
    //用户输入表达式或者计算结果字符串
    var inputString = ""
    //历史表达式字符串
    var historyString = ""
    //所有数字字符 用于进行检测匹配
    let figureArray:Array<Character> = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "."]
    //所有运算功能字符 用于进行检测匹配
    let funcArray = ["+", "-", "*", "/", "^"]
    init() {
        super.init(frame: CGRect.zero)
        inputLabel = UILabel()
        historyLabel = UILabel()
        setupUI()
    }
    
    func setupUI() {
        //设置文字的对其方式为右对齐
        inputLabel?.textAlignment = .right
        historyLabel?.textAlignment = .right
        //设置字体
        inputLabel?.font = UIFont.systemFont(ofSize: 34)
        historyLabel?.font = UIFont.systemFont(ofSize: 30)
        //设置文字颜色
        inputLabel?.textColor = UIColor.orange
        historyLabel?.textColor = UIColor.black
        //设置文字大小根据字数进行适配
        inputLabel?.adjustsFontSizeToFitWidth = true
        inputLabel?.minimumScaleFactor = 0.5  //最小字体为当前字体的一半
        historyLabel?.adjustsFontSizeToFitWidth = true
        historyLabel?.minimumScaleFactor = 0.5
        //设置文字的截断方式
        inputLabel?.lineBreakMode = .byTruncatingHead
        historyLabel?.lineBreakMode = .byTruncatingHead
        //设置文字的行数
        inputLabel?.numberOfLines = 0
        historyLabel?.numberOfLines = 0
        
        self.addSubview(inputLabel!)
        self.addSubview(historyLabel!)
        //进行自动布局
        inputLabel?.snp.makeConstraints({ (make) in
            make.left.equalTo(10)
            make.right.equalTo(-10)
            make.bottom.equalTo(-10)
        make.height.equalTo(inputLabel!.superview!.snp.height).multipliedBy(0.5).offset(-10)
        })
        historyLabel?.snp.makeConstraints({ (make) in
            make.left.equalTo(10)
            make.right.equalTo(-10)
            make.top.equalTo(10)
            make.height.equalTo(inputLabel!.superview!.snp.height).multipliedBy(0.5).offset(-10)
        })
    }
    
    //提供一个输入信息的接口
    func inputContent(content:String) {
        inputString.append(content)
        inputLabel?.text = inputString
    }
    
    //提供一个刷新历史记录的方法
    func refreshHistory() {
        historyString = inputString
        historyLabel?.text = historyString
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
}

DWBoard类可以接收用户的输入,DWScreen需要获取用户的输入,他们之间的关联是需要通过ViewController类来完成的。使用代理设计模式完成此功能。

在DWBoard.swift添加协议代码

protocol DWBoardButtonInputDelegate {
    func boardButtonClick(content:String)
}

在DWBoard类添加一个代理属性:

var delegate:DWBoardButtonInputDelegate?

修改DWBoard类中的点击事件

@objc func btnClick(_ button:DWFuncButton) {
    if delegate != nil {
        //通过协议方法将值传递出去
        delegate?.boardButtonClick(content: button.currentTitle!)
    }
}

ViewController类也需要将DWBoard类实例和DWScreen类实例作为自己的属性,相互调用

let board = DWBoard()
let screen = DWScreen()

在viewDidLoad()方法添加setupUI(),并且setupUI代码如下

func setupUI() {
    self.view.addSubview(board)
    //设置代理
    board.delegate = self
    board.snp.makeConstraints { (make) in
        make.left.equalTo(0)
        make.right.equalTo(0)
        make.bottom.equalTo(0)
        make.height.equalTo(board.superview!.snp.height).multipliedBy(2/3.0)
    }
    
    self.view.addSubview(screen)
    screen.snp.makeConstraints { (make) in
        make.left.equalTo(0)
        make.right.equalTo(0)
        make.top.equalTo(0)
        make.bottom.equalTo(board.snp.top)
    }
}

viewController需要遵守DWBoardButtonInputDelegate协议:

class ViewController: UIViewController,DWBoardButtonInputDelegate

并且实现协议方法:

func boardButtonClick(content: String) {
    if content == "AC" || content == "DEL" || content == "=" {
        //进行逻辑处理
        screen.refreshHistory()
    }else {
        screen.inputContent(content:content)
    }
}

运行项目,如下图:


界面部分我们已经基本开发完,接下来进行逻辑处理类的封装。

三、计算器计算逻辑:
DWScreen类需要继续完善。例如当用户点击清空按钮时,输入的计算表达就应该被清空。当用户点击回退按钮时,上一次输入的字符就应该被清空。在DWScreen类添加如下代码:

//清空显示屏当前输入的信息
func clearContent() {
    inputString = ""
}

//删除显示屏中上次输入的字符
func deleteInput() {
    if inputString.characters.count>0 {
        inputString.remove(at: inputString.index(before: inputString.endIndex))
        inputLabel?.text = inputString
    }
}

在项目中新建一个继承于NSObject的类文件,并命名为DWCalculatorEngine。将其作为计算引擎工具类,代码如下:

class DWCalculatorEngine: NSObject {
    //运算符集合
    let funcArray:CharacterSet = ["+", "-", "*", "/", "^", "%"]
    func calculatEquation(equation:String)->Double {
        //以运算符进行分割获取到所有数字
        let elementArray = equation.components(separatedBy: funcArray)
        //设置一个运算标记游标
        var tip = 0
        //运算结果
        var result:Double = Double(elementArray[0])!
        //遍历计算表达式
        for char in equation.characters {
            switch char {
                //进行加法运算
            case "+":
                tip += 1
                if elementArray.count>tip {
                    result += Double(elementArray[tip])!
                }
                //进行减法运算
            case "-":
                tip += 1
                if elementArray.count>tip {
                    result -= Double(elementArray[tip])!
                }
            case "*":
                tip += 1
                if elementArray.count>tip {
                    result *= Double(elementArray[tip])!
                }
                //进行除法运算
            case "/":
                tip += 1
                if elementArray.count>tip {
                    result /= Double(elementArray[tip])!
                }
                //进行取余运算
            case "%":
                tip += 1
                if elementArray.count>tip {
                    result = Double(Int(result)%Int(elementArray[tip])!)
                }
                //进行指数运算
            case "^":
                tip += 1
                if elementArray.count>tip {
                    let tmp = result
                    for _ in 1..<Int(elementArray[tip])! {
                        result *= tmp
                    }
                }
            default:
                break
            }
        }
        return result
    }
}

在ViewController类中添加两个属性:一个计算工具类:

//计算引擎实例
let calcalator = DWCalculatorEngine()
//这个输入是否需要刷新显示屏
var isNew = false

isNew属性主要作用是标记本次输入是否需要将显示屏已有的内容清除。当用户完成一次计算后,计算结果会显示在显示屏上。此时如果用户继续输入,则进行下一轮的计算,显示屏的上次结果应该被清空。
修改ViewController类中的协议方法

func boardButtonClick(content: String) {
    if content == "AC" || content == "DEL" || content == "=" {
        //进行逻辑处理
        switch content {
        case "AC":
            screen.clearContent()
            screen.refreshHistory()
        case "DEL":
            screen.deleteInput()
        case "=":
            let result = calcalator.calculatEquation(equation: screen.inputString)
            //先刷新历史
            screen.refreshHistory()
            //清除输入的内容
            screen.clearContent()
            //将结果输入
            screen.inputContent(content: String(result))
            isNew = true
        default:
            screen.refreshHistory()
        }
        
    }else {
        if isNew {
            screen.clearContent()
            isNew = false
        }
        screen.inputContent(content:content)
    }
}

大功告成!代码传送门

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,175评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,674评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,151评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,597评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,505评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,969评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,455评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,118评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,227评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,213评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,214评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,928评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,512评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,616评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,848评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,228评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,772评论 2 339

推荐阅读更多精彩内容