11.2/12:GLSL 加载图片 (Swift)

  • 最终效果:
倒着显示的图片

1. 流程总览

  • 口述:

    • 给 GPU 执行的代码叫做 Shader,我们写完 Shader 之后要告诉 GPU 使用它。
    • 给 GPU 执行的数据,出生在内存,由我们 copy 到显存,并告诉 Shader 代码如何读取这些数据。
    • FrameBuffer 是最终显示的内容,但它本身不保存内容,而是指向保存内容的 RenderBuffer (或 纹理)。
    • GPU 的 Shader 代码处理显存数据得出的结果,存储在目标 RenderBuffer。
    • 系统提供的窗口相关的方法和设置都在 Layer。
    • 以上复杂关系由状态机 Context 进行管理,包括但不限于:
      • GPU 用哪个 Shader
      • FrameBuffer 指向哪个 RenderBuffer
      • Shader 的执行结果存储在哪个 RenderBuffer
      • Layer 何时切换显示数据 (FrameBuffer / RenderBuffer)
  • 下图是 GLSL 加载图片的全流程 (建议下载原图食用),很实在的面向过程。看不懂,很正常,拿着这个图继续看到后面,应该会有点感觉。

11 iOS-OpenGLES加载图片基本流程.jpg

2. 前置知识

2.1. Frame Buffer Object (FBO)

更详细见:帧缓存对象 Frame Buffer Object
作者:无名小基
https://blog.csdn.net/u014767384/article/details/81810388

  • Frame Buffer可以意会为图像显示的最终表达者,但它本身不存储图像信息,而是指向存储图像信息的地方:Render Buffer 或 纹理。

  • Render Buffer和纹理都是分为3个类型:

    • 颜色color
    • 深度depth
    • 模板stencil
  • Frame Buffer关联Render Buffer的函数为例,第1/3参数是不可改的,第4参数是Render Buffer对象,第2参数是Frame Buffer的附着点,有 颜色(0-15) / 深度(只有1个) / 模板(只有1个)。

    同一个Frame Buffer里所有的附着点的采样数必须一样。

    // 4. Framebuffer <=挂载 GL_RENDERBUFFER
    glFramebufferRenderbuffer(
        GLenum(GL_FRAMEBUFFER), //指定当前Framebuffer类型,其他2种:GL_DRAW_FRAMEBUFFER GL_READ_FRAMEBUFFER
        GLenum(GL_COLOR_ATTACHMENT0), //指定纹理对象在Framebuffer的挂载点,COLOR区可选0-15,其他3种:GL_DEPTH_ATTACHMENT GL_STENCIL_ATTACHMENT GL_DEPTH_STENCIL_ATTACHMENT
        GLenum(GL_RENDERBUFFER), //指定Renderbuffer类型,必须是GL_RENDERBUFFER
        myColorRenderBuffer) //指定Renderbuffer的对象
    
  • 系统可同时存在多个Frame Buffer,能够随时切换不同的Frame Buffer。然而,条件允许的话,性能更高的做法是Frame Buffer指向不同的Render Buffer 或 纹理。

2.2. 图片解压缩

平时我们使用的 jpg png 是压缩过的图片,要转成位图才能作为纹理传入显存给GPU使用。代码很简单,就是分别设置好 原图/目标位图 的图片格式,然后调用系统方法 重新绘制一个位图。

3. Shader 文件

3.1. 创建

image.png
image.png

后缀名可以改,不影响执行

3.2. shaderv.vsh 顶点着色器

// attribute >> 顶点坐标
attribute vec4 position;

// attribute >> 纹理坐标
attribute vec2 texCoordinate;

// 顶点着色器 => 片元着色器的通道,vsh和fsh 里的声明必须完全相同
varying lowp vec2 varyTexCoord;

void main() {
    // 纹理坐标 通过varyTexCoord 传递到 片元着色器
    varyTexCoord = texCoordinate;
    
    // 顶点坐标 (内建变量)
    gl_Position = position;
}

3.3. shaderf.fsh 片元着色器

// float 使用高精度,下面如果有 float 使用低精度,必须加 lowp
precision highp float;

// 顶点着色器 => 片元着色器的通道,vsh和fsh 里的声明必须完全相同
varying lowp vec2 varyTexCoord;

// uniform >> 纹理
uniform sampler2D colorMap;

void main() {
    // 1. 纹素:纹理在对应坐标的颜色值
    lowp vec4 pixelTex = texture2D(colorMap, varyTexCoord);
    // 2. 片元像素颜色 (内建变量)
    gl_FragColor = pixelTex;
}

4. MyView.swift

4.1. 概览 (速记)

override func layoutSubviews()里使用 5步准备 + 1步绘制:

  1. setupLayer()

    • self.layer转成CAEAGLLayer并保存为成员属性,来使用CALayer没有的EAGLDrawable接口 以及 CAEAGLLayer的一些方法。

    • 设置像素精度self.contentScaleFactor

    • 设置绘制参数myEAGLLayer.drawableProperties

      • 绘图表面显示后,是否保留其内容。
      • 可绘制表面的内部颜色缓存区格式。
  2. setupContext()

    • 创建EAGLContext,并EAGLContext.setCurrent(xxx)
  3. deleteRenderAndFrameBuffer()

    • 清空 RenderBuffer 和 FrameBuffer。
  4. setupRenderBuffer()

    • var/Gen/Bind

    • 通过context(内部调用了GL方法) 申请与Layer的显存空间,然后 绑定=> GL_RENDERBUFFER

  5. setupFrameBuffer()

    • var/Gen/Bind

    • Framebuffer <=挂载 GL_RENDERBUFFER

  6. renderLayer()

    • 清屏颜色,清颜色缓冲区

    • 设置视口

    • 创建 program,shader编译后 附着到program,取得programID

    • 链接 program

    • 使用 program

    • 准备顶点数据,内存 copy=> 显存:var/Gen/Bind/Data

    • 设置某个attribute 读取 顶点坐标 的方式:Location/Enable/Pointer

    • 设置某个attribute 读取 纹理坐标 的方式:Location/Enable/Pointer

    • 加载纹理:图片 解压=> 位图 copy=> 显存的纹理,返回纹理ID

    • 使用某uniform 传纹理:Location/Uniform

    • 绘制:glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)

    • RenderBuffer 显示=> 屏幕:myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))

4.2. 代码

import UIKit

@available(iOS, deprecated: 12.0)
class MDView: UIView {
    
    var myEAGLLayer: CAEAGLLayer!
    
    var myContext: EAGLContext!
    
    var myColorRenderBuffer: GLuint = 0
    
    var myColorFrameBuffer: GLuint = 0
    
    var myProgram: GLuint = 0
    
    // 顶点索引:bind之后对应 Shader的 Attribute/Uniform名
    enum GLSLLocation {
        enum Attribute: UInt32 {
            case Position
            case TexCoordinate
        }
        enum Uniform: UInt32 {
            case ColorMap
        }
    }
    
    
    override func layoutSubviews() {
        
        // 5步准备工作
        setupLayer()
        setupContext()
        deleteRenderAndFrameBuffer()
        setupRenderBuffer()
        setupFrameBuffer()
        
        // 绘制
        renderLayer()
    }
    
    
    // 如果不写,默认返回 CALayer.self,导致 myEAGLayer = layer as? CAEAGLLayer 失败
    override class var layerClass: AnyClass {
        return CAEAGLLayer.self
    }
    
    
    // 1. 设置图层
    func setupLayer() {
        
        // 1. 初始化 layer
        myEAGLLayer = layer as? CAEAGLLayer
        
        if myEAGLLayer == nil {
            print("Convert Layer to CAEAGLLayer failed!!")
            return
        }
        
        // 2. 设置 scale,1.0 或 2.0(此处)。
        //    可以理解为像素精度,譬如某个view的size是50x50,scalefactor=2.0,位图要用到100x100像素。
        contentScaleFactor = UIScreen.main.scale

        // 3. 设置绘制参数 (总共也是2个):
        myEAGLLayer.drawableProperties = [
            // 绘图表面显示后,是否保留其内容。
            kEAGLDrawablePropertyRetainedBacking : false,
            // 可绘制表面的内部颜色缓存区格式
            kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8
        ]
    }
    
    
    // 2. 设置上下文
    func setupContext() {
        
        myContext = EAGLContext(api: .openGLES3)
        
        if myContext == nil {
            print("Create EAGLContext failed!!")
            return
        }
        
        guard EAGLContext.setCurrent(myContext) else {
            print("Set current EAGLContext failed!!")
            return
        }
    }
    
    
    // 3. 清空缓冲区
    func deleteRenderAndFrameBuffer() {
        
        // 清空 RenderBuffer 和 FrameBuffer
        glDeleteRenderbuffers(1, &myColorRenderBuffer)
        myColorRenderBuffer = 0
        
        glDeleteFramebuffers(1, &myColorFrameBuffer)
        myColorFrameBuffer = 0
    }
    
    
    // 4. 创建/绑定 RenderBuffer
    func setupRenderBuffer() {
        
        // 1. 定义缓存区ID
        var buffer: GLuint = 0
        
        // 2. 申请缓存区标志
        glGenRenderbuffers(1, &buffer)
        
        myColorRenderBuffer = buffer
        
        // 3. 标志(新申请的缓存区) 绑定=> GL_RENDERBUFFER
        glBindRenderbuffer(GLenum(GL_RENDERBUFFER), myColorRenderBuffer)
        
        // 4. 通过context(内部调用了GL方法) 申请 Layer 匹配的显存空间,然后 绑定=> GL_RENDERBUFFER
        myContext.renderbufferStorage(Int(GL_RENDERBUFFER), from: myEAGLLayer)
        /*
         第4步 替代了
         glRenderbufferStorage(GLenum target,
                               GLenum internalformat,
                              GLsizei width,
                              GLsizei height)
         2个方法都能用来申请显存空间,iOS方法通过EAGLLayer计算得来,而GL方法根据参数设置。
         */
    }
    
    
    // 5. FrameBuffer
    func setupFrameBuffer() {
        
        // 1. 定义缓存区ID
        var buffer: GLuint = 0
        
        // 2. 申请缓存区标志
        glGenFramebuffers(1, &buffer)
        
        myColorFrameBuffer = buffer
        
        // 3. 标志(新申请的缓存区) 绑定=> GL_RENDERBUFFER
        glBindFramebuffer(GLenum(GL_FRAMEBUFFER), myColorFrameBuffer)
        
        // 4. Framebuffer <=挂载 GL_RENDERBUFFER
        glFramebufferRenderbuffer(
            GLenum(GL_FRAMEBUFFER), //指定当前Framebuffer类型,其他2种:GL_DRAW_FRAMEBUFFER GL_READ_FRAMEBUFFER
            GLenum(GL_COLOR_ATTACHMENT0), //指定纹理对象在Framebuffer的挂载点,COLOR区可选0-15,其他3种:GL_DEPTH_ATTACHMENT GL_STENCIL_ATTACHMENT GL_DEPTH_STENCIL_ATTACHMENT
            GLenum(GL_RENDERBUFFER), //指定Renderbuffer类型,必须是GL_RENDERBUFFER
            myColorRenderBuffer) //指定Renderbuffer的对象
    }
    

    // 6. 绘制
    func renderLayer() {
        
        
        // 1. 清屏颜色(浅红色),清颜色缓冲区
        glClearColor(0.5, 0.1, 0.1, 0.5)
        glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
        
        // 2. 设置视口
        let scale = UIScreen.main.scale
        glViewport(GLint(frame.origin.x * scale),
                   GLint(frame.origin.y * scale),
                   GLsizei(frame.size.width * scale),
                   GLsizei(frame.size.height * scale))
        
        // 3. 获取文件Bundle路径:顶点着色器/片元着色器
        guard let vFilePath = Bundle.main.path(forAuxiliaryExecutable: "shaderv.vsh") else {
            print("shaderv.vsh not found")
            return
        }
        guard let fFilePath = Bundle.main.path(forAuxiliaryExecutable: "shaderf.fsh") else {
            print("shaderf.fsh not found")
            return
        }
        
        // 4. 创建program,取得programID:
        // 4.1. filename => file => String => GLchar* 附着=> shader => shader编译
        // 4.2. 创建program => vShader/fShader 分别附着=> program => 释放vShader/fShader
        myProgram = loaderShaders(vShaderFilePath: vFilePath, fShaderFilePath: fFilePath)
        
        
        /*
         该步骤等同第10步,但必须在 glLinkProgram 之前执行
         */
        glBindAttribLocation(
            myProgram,
            GLSLLocation.Attribute.TexCoordinate.rawValue,
            "texCoordinate")
        
        // 5. 链接 program
        glLinkProgram(myProgram)
        
        // 获取链接状态,做容错
        var linkStatus: GLint = 0
        glGetProgramiv(myProgram, GLenum(GL_LINK_STATUS), &linkStatus)
        
        if linkStatus == GL_FALSE {
            // Link 失败信息
            var message = [GLchar](repeating: 96, count: 1024)
            var length: GLsizei = 0
            glGetProgramInfoLog(
                myProgram,
                GLsizei(MemoryLayout.stride(ofValue: message)), //存储log的buffer大小
                &length, //获取到log信息的长度
                &message) //获取到log信息

            guard let messageStr = String(cString: message, encoding: .utf8) else {
                print("Error when getting Link-failed log!!")
                return
            }
            print(messageStr)
            return
        }
        
        // 6. 使用 program,着色器代码准备就绪
        glUseProgram(myProgram)
        
        
        // 7. 顶点数据:顶点坐标/纹理坐标
        let attrArr: [GLfloat] =
        [
             0.9, -0.3, -1.0,     1.0, 0.0,
            -0.9,  0.3, -1.0,     0.0, 1.0,
            -0.9, -0.3, -1.0,     0.0, 0.0,
            
             0.9,  0.3, -1.0,     1.0, 1.0,
            -0.9,  0.3, -1.0,     0.0, 1.0,
             0.9, -0.3, -1.0,     1.0, 0.0,
        ]
        GL_ELEMENT_ARRAY_BUFFER
        // 8. 内存 copy=> 显存
        var attrBuffer: GLuint = 0
        glGenBuffers(1, &attrBuffer)
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), attrBuffer)
        glBufferData(
            GLenum(GL_ARRAY_BUFFER),
            MemoryLayout<GLfloat>.stride * attrArr.count,
            attrArr,
            GLenum(GL_STATIC_DRAW))
        
        // 9. 设置某个attribute 读取 顶点坐标 的方式。
        // 9.1. 获取program的某个attribute通道ID,这里是"position",要和shader文件里的命名完全一致。
        let position = GLuint(glGetAttribLocation(myProgram, "position"))
        
        // 9.2. 默认情况下,出于性能考虑 vshader的 attribute变量 都是关闭的,
        //      即使数据到了GPU,如果不打开通道,该数据对 GPU(GPU的shader代码)就是不可见的。
        //      该方法也可在glVertexAttribPointer之后调用,但必须早于 glDraw系列方法。
        glEnableVertexAttribArray(position)
        
        // 9.3. 设置读取方式
        glVertexAttribPointer(
            position, //index:数据通道的索引
            3, //size:每个顶点用多少个数据描述,默认是4个
            GLenum(GL_FLOAT), //type:数据类型,可用 GL_BYTE GL_SHORT,默认GL_FLOAT
            GLboolean(GL_FALSE), //是否归一化 或 直接转换为固定值
            GLsizei(MemoryLayout<GLfloat>.stride * 5), //stride:步长
            UnsafePointer(bitPattern: 0)) //ptr:偏移
        
        
        // 10. 设置某个attribute 读取 纹理坐标 的方式。同第9步。但本次使用 bind(第4/5步之间)
        //let texCoordinate = GLuint(glGetAttribLocation(myProgram, "texCoordinate"))
        
        //glEnableVertexAttribArray(texCoordinate)
        glEnableVertexAttribArray(GLSLLocation.Attribute.TexCoordinate.rawValue)
        
        glVertexAttribPointer(
            GLSLLocation.Attribute.TexCoordinate.rawValue,
            2,
            GLenum(GL_FLOAT),
            GLboolean(GL_FALSE),
            GLsizei(MemoryLayout<GLfloat>.stride * 5),
            UnsafePointer(bitPattern: MemoryLayout<GLfloat>.stride * 3))
        
        
        // 11. 加载纹理:
        //     图片 解压=> 位图 copy=> 显存的纹理,返回纹理ID
        let textureID = setupTexture(fileName: "pcr.jpg")
        
        // 12. 使用某uniform 传纹理,通道默认打开,不用去Enable
        let colorMap = glGetUniformLocation(myProgram, "colorMap")
        
        glUniform1i(
            colorMap, //index:数据通道的索引
            textureID) //纹理ID
        
        // 13. 绘制:图元=三角形,从第0个顶点开始绘制,共6个顶点
        glDrawArrays(GLenum(GL_TRIANGLES), 0, 6)
        
        // 14.显示
        myContext.presentRenderbuffer(Int(GL_RENDERBUFFER))
    }
    
    
    /// 图片 解压=> 位图 copy=> 显存的纹理,返回纹理ID
    /// - Parameter fileName: 图片文件名
    /// - Returns: 纹理ID
    func setupTexture(fileName: String) -> GLint {
        
        // CGImage
        guard let spriteImg = UIImage(named: fileName)?.cgImage else {
            print("Fail to load image.")
            return 0
        }
        
        
        // 原图的颜色空间
        guard let colorSpace = spriteImg.colorSpace else {
            print("Fail to get colorSpace.")
            return 0
        }
        
        // 图片原宽高
        let width = spriteImg.width
        let height = spriteImg.height
        
        // 每个像素颜色所需内存:RGBA(4位) 都占用 1个GLubyte
        let pixelStride = MemoryLayout<GLubyte>.stride * 4
        // 所有像素颜色所需内存
        let totalStride = pixelStride * width * height
        
        // 申请内存空间,重新绘制好的位图就保存在这里
        let spriteData = UnsafeMutablePointer<GLubyte>.allocate(capacity: totalStride)
        
        // https://developer.apple.com/documentation/coregraphics/cgcontext/1455939-init
        // 创建  CGContext
        let spriteContext = CGContext(
            data: spriteData, //存储位置的引用,重新绘制好的位图就保存在这里
            width: width, //将来位图的宽
            height: height, //将来位图的高
            bitsPerComponent: 8, //这次使用的32位的RGBA,每一位占用内存8位(是bit不是byte);若64则对应16
            bytesPerRow: pixelStride * width, //每行占用内存n字节
            space: colorSpace, //将来位图的颜色空间,不能是 索引颜色空间
            bitmapInfo: spriteImg.bitmapInfo.rawValue) //alpha位的位置 & RGB位是否提前乘以alpha
        
        
        // CGContext 绘图
        let rect = CGRect(x: 0, y: 0, width: width, height: height)
        spriteContext?.draw(spriteImg, in: rect)

        
        // ----- 以上解压完成,下面把位图从 内存 copy=> 显存 -----
        

        // 绑定纹理到ID
        var textureID: GLuint = 0
        glGenTextures(1, &textureID)
        glBindTexture(GLenum(GL_TEXTURE_2D), textureID)
        glActiveTexture(GLenum(GL_TEXTURE0 + Int32(textureID)))
        /*
         由于 ID = 0 的纹理区 默认一直处于激活状态,这里也只用到1个纹理ID,以上4行可以替换成:
         glBindTexture(GLenum(GL_TEXTURE_2D), 0)
         
         也因此,上面自己 glBindTexture的 textureID 总是从1开始,而不是0,
         之后的 glActiveTexture 指定的 texturei 也要从 GL_TEXTURE0 开始加。
        */
        
        // 设置纹理属性,缩小/放大的过滤方式,S/T轴的环绕方式
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MIN_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_MAG_FILTER), GL_LINEAR)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_S), GL_CLAMP_TO_EDGE)
        glTexParameteri(GLenum(GL_TEXTURE_2D), GLenum(GL_TEXTURE_WRAP_T), GL_CLAMP_TO_EDGE)
        
        // 载入纹理:内存 copy=> 显存
        glTexImage2D(
            GLenum(GL_TEXTURE_2D), //纹理模式:1D 2D 3D
            0, //level:指定详细程度编号,0 是基本图像级别,级别n是第n个缩略图缩小图像
            GL_RGBA, //internalformat:纹理的内部样式,GL_ALPHA GL_LUMINANCE GL_LUMINANCE_ALPHA GL_RGB GL_RGBA
            GLsizei(width), //纹理的宽
            GLsizei(height), //纹理的高
            0, //border:边框宽度,必须是0。
            GLenum(GL_RGBA), //图像在内存的样式,大端/小端设备 分别对应 ARGB/BGRA,反正转换后必须与internalformat保持一致
            GLenum(GL_UNSIGNED_BYTE), //图像在内存的数据类型
            spriteData) //内存中存放图像数据的指针
        
        
        // 释放 spriteData
        free(spriteData)
        
        return GLint(textureID)
    }
    
    
    
    // MARK:- shader
    
    
    /// 用 顶点着色器/片元着色器 生成 program,返回 programID。
    /// - Parameters:
    ///   - vShaderFilePath: 顶点着色器的Bundle文件路径
    ///   - fShaderFilePath: 片元着色器的Bundle文件路径
    /// - Returns: programID
    func loaderShaders(vShaderFilePath: String, fShaderFilePath: String) -> GLuint {
        
        // 1. 定义 顶点着色器/片元着色器对象
        var vShader: GLuint = 0
        var fShader: GLuint = 0
        
        // 2. 创建一个空的 program
        let program = glCreateProgram()
        
        // 3. 编译 顶点着色器/片元着色器
        compileShader(shader: &vShader, type: GLenum(GL_VERTEX_SHADER), filePath: vShaderFilePath)
        compileShader(shader: &fShader, type: GLenum(GL_FRAGMENT_SHADER), filePath: fShaderFilePath)
        
        // 4. 顶点着色器/片元着色器 编译后的着色器代码 附着=> program
        glAttachShader(program, vShader)
        glAttachShader(program, fShader)

        // 5. 释放 shader
        glDeleteShader(vShader)
        glDeleteShader(fShader)
        
        return program
    }
    
    
    /// 把着色器源码绑定到shader,并编译成目标代码。
    /// - Parameters:
    ///   - shader: id,编译完成后会被赋值
    ///   - type: 着色器类型
    ///   - file: 着色器文件名
    func compileShader(shader: inout GLuint, type: GLenum, filePath: String) {
        
        
        // 1. filePath => Data => String => [GLchar] => 指针
        
        guard let content = FileManager.default.contents(atPath: filePath) else {
            print("File not found!!")
            return
        }
        
        guard let contentStr = String(data: content, encoding: .utf8) else {
            print("Conver Data to String failed!!")
            return
        }
        
        // String.cString() => [GLchar]
        // typealias  CChar = Int8
        // typealias GLchar = Int8
        guard let contentChar: [GLchar] = contentStr.cString(using: .utf8) else {
            print("Conver String to [GLchar] failed!!")
            return
        }
        
        /*
         指针的变换:
         [GLchar]
         => UnsafeMutablePointer<GLchar>
         => UnsafePointer<GLchar>?
         这次直接使用符号 &,本质与封装2次指针相同 ↓
         => UnsafeMutablePointer<UnsafePointer<GLchar>?>
         => UnsafePointer<UnsafePointer<GLchar>?>
         
         */
        let mutOncePointer = UnsafeMutablePointer<GLchar>.allocate(capacity: contentChar.count)
        mutOncePointer.initialize(from: contentChar, count: contentChar.count)
        
        var oncePointer = UnsafePointer?(mutOncePointer)
        
        // 2. 创建一个对应类型的空的 shader对象
        shader = glCreateShader(type)
        
        // 3. 将着色器源码GLchar 附着=> shader对象
        glShaderSource(shader, 1, &oncePointer, nil)
        
        
        /*
         以上 1.2.3.步骤有简略写法,参考自
         https://blog.csdn.net/lin1109221208/article/details/107733156
         里面的GitHub代码
         */
        contentStr.withCString { (ccharPointer) in
            var pointer: UnsafePointer<GLchar>? = ccharPointer
            //glShaderSource(shader, 1, &pointer, nil)
        }

        
        // 4. 把着色器源码GLchar 编译成=> 目标代码
        glCompileShader(shader)
    }
    
    
}

5. Main.storyboard 文件

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