2016-05-06 11 views
1

У меня есть простой пользовательский CALayer, чтобы создать эффект наложения градиента на моем UIView. Вот код:Подслои слоя UIView отображаются по-разному/случайным образом, каждый раз появляется вид

class GradientLayer: CALayer { 

var locations: [CGFloat]? 
var origin: CGPoint? 
var radius: CGFloat? 
var color: CGColor? 

convenience init(view: UIView, locations: [CGFloat]?, origin: CGPoint?, radius: CGFloat?, color: UIColor?) { 
    self.init() 
    self.locations = locations 
    self.origin = origin 
    self.radius = radius 
    self.color = color?.CGColor 
    self.frame = view.bounds 
} 

override func drawInContext(ctx: CGContext) { 
    super.drawInContext(ctx) 

    guard let locations = self.locations else { return } 
    guard let origin = self.origin else { return } 
    guard let radius = self.radius else { return } 

    let colorSpace = CGColorGetColorSpace(color) 
    let colorComponents = CGColorGetComponents(color) 

    let gradient = CGGradientCreateWithColorComponents(colorSpace, colorComponents, locations, locations.count) 
    CGContextDrawRadialGradient(ctx, gradient, origin, CGFloat(0), origin, radius, [.DrawsAfterEndLocation]) 
} 
} 

Я инициализировать и установить эти слои здесь:

override func viewWillLayoutSubviews() { 
    super.viewWillLayoutSubviews() 
    let gradient1 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX, y: view.frame.midY), radius: 100.0, color: UIColor(white: 1.0, alpha: 0.2)) 

    let gradient2 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX-20, y: view.frame.midY+20), radius: 160.0, color: UIColor(white: 1.0, alpha: 0.2)) 

    let gradient3 = GradientLayer(view: view, locations: [0.0,1.0], origin: CGPoint(x: view.frame.midX+30, y: view.frame.midY-30), radius: 300.0, color: UIColor(white: 1.0, alpha: 0.2)) 

    gradient1.setNeedsDisplay() 
    gradient2.setNeedsDisplay() 
    gradient3.setNeedsDisplay() 

    view.layer.addSublayer(gradient1) 
    view.layer.addSublayer(gradient2) 
    view.layer.addSublayer(gradient3) 
} 

мнение кажется отображались правильно большую часть времени, но (на первый взгляд) случайным образом я буду получать различные визуализаций как вы увидите ниже. Вот некоторые примеры (первый из них является то, что я хочу):

enter image description here enter image description here enter image description here

Что является причиной этой неисправности? Как загружать первый раз каждый раз?

ответ

1

У вас есть несколько проблемы.

Во-первых, вы должны думать о градиенте, как массив останавливается, где остановка состоит из двух частей: цвет и расположение. У вас должно быть одинаковое количество цветов и мест, потому что каждая остановка имеет по одному. Вы можете увидеть это, если, например, нужно проверить документацию CGGradientCreateWithColorComponents относительно components аргумента:

Число элементов в этом массиве должен быть продуктом count и количество компонентов в цветовом пространстве.

Это продукт (результат умножения), потому что у вас есть count, и вам нужен полный набор цветовых компонентов для каждой остановки.

Вы не обеспечиваете достаточное количество компонентов цвета. Ваш GradientLayer может иметь любое количество мест (и вы даете ему два), но имеет только один цвет. Вы получаете компоненты одного цвета и передаете их как массив компонентов в CGGradientCreateWithColorComponents, но массив слишком короткий. Swift не поймает эту ошибку - обратите внимание, что тип вашего colorComponents - UnsafePointer<CGFloat>. Часть Unsafe сообщает вам, что вы находитесь на опасной территории. (Вы можете увидеть тип colorComponents при помощи опций, щелкнув его в Xcode.)

Поскольку вы не обеспечивает достаточно большой массив для components, IOS использует любые случайные величины оказываются в памяти после того, как компонент вашего один цвет. Они могут меняться от запуска до бега и часто не то, что вы хотите, чтобы они были.

Фактически, вы даже не должны использовать CGGradientCreateWithColorComponents. Вы должны использовать CGGradientCreateWithColors, который принимает массив CGColor, поэтому он не только прост в использовании, но и безопаснее, потому что он меньше UnsafePointer, плавающий вокруг.

Вот что GradientLayer должен выглядеть следующим образом:

class RadialGradientLayer: CALayer { 

    struct Stop { 
     var location: CGFloat 
     var color: UIColor 
    } 

    var stops: [Stop] { didSet { self.setNeedsDisplay() } } 
    var origin: CGPoint { didSet { self.setNeedsDisplay() } } 
    var radius: CGFloat { didSet { self.setNeedsDisplay() } } 

    init(stops: [Stop], origin: CGPoint, radius: CGFloat) { 
     self.stops = stops 
     self.origin = origin 
     self.radius = radius 
     super.init() 
     needsDisplayOnBoundsChange = true 
    } 

    override init(layer other: AnyObject) { 
     guard let other = other as? RadialGradientLayer else { fatalError() } 
     stops = other.stops 
     origin = other.origin 
     radius = other.radius 
     super.init(layer: other) 
    } 

    required init?(coder aDecoder: NSCoder) { 
     fatalError("init(coder:) has not been implemented") 
    } 

    override func drawInContext(ctx: CGContext) { 
     let locations = stops.map { $0.location } 
     let colors = stops.map { $0.color.CGColor } 

     locations.withUnsafeBufferPointer { pointer in 
      let gradient = CGGradientCreateWithColors(nil, colors, pointer.baseAddress) 
      CGContextDrawRadialGradient(ctx, gradient, origin, 0, origin, radius, [.DrawsAfterEndLocation]) 
     } 
    } 

} 

Следующая проблема. Вы добавляете больше слоев градиента каждый раз, когда система вызывает viewWillLayoutSubviews. Он может вызывать эту функцию несколько раз! Например, он будет вызывать его, если ваше приложение поддерживает вращение интерфейса, или если приходит вызов, а iOS делает панель состояния более высокой. (Вы можете проверить это в симуляторе, выбрав «Оборудование»> «Переключить строку состояния во время звонка».)

Необходимо создать слои градиента один раз, сохранив их в свойстве. Если они уже созданы, вам необходимо обновить их фреймы и не создавать новые слои:

class ViewController: UIViewController { 

    private var gradientLayers = [RadialGradientLayer]() 

    override func viewWillLayoutSubviews() { 
     super.viewWillLayoutSubviews() 

     if gradientLayers.isEmpty { 
      createGradientLayers() 
     } 

     for layer in gradientLayers { 
      layer.frame = view.bounds 
     } 
    } 

    private func createGradientLayers() { 
     let bounds = view.bounds 
     let mid = CGPointMake(bounds.midX, bounds.midY) 
     typealias Stop = RadialGradientLayer.Stop 

     for (point, radius, color) in [ 
      (mid, 100, UIColor(white:1, alpha:0.2)), 
      (CGPointMake(mid.x - 20, mid.y + 20), 160, UIColor(white:1, alpha:0.2)), 
      (CGPointMake(mid.x + 30, mid.y - 30), 300, UIColor(white:1, alpha:0.2)) 
      ] as [(CGPoint, CGFloat, UIColor)] { 

       let stops: [RadialGradientLayer.Stop] = [ 
        Stop(location: 0, color: color), 
        Stop(location: 1, color: color.colorWithAlphaComponent(0))] 

       let layer = RadialGradientLayer(stops: stops, origin: point, radius: radius) 
       view.layer.addSublayer(layer) 
       gradientLayers.append(layer) 
     } 
    } 

} 
+0

Не могли бы вы объяснить, для чего нужен 'init (layer other: AnyObject)'? – Aaron

+1

Из документации: «Этот инициализатор используется для создания теневых копий слоев, например, для метода« presentationLayer ». [...] Если вы реализуете подклассы пользовательского уровня, вы можете переопределить этот метод и использовать его для копирования значений переменных экземпляра в новый объект ». –

0

Ваша проблема заключается в код, который вы написали в viewWillLayoutSubviews ее функция вызывается несколько раз, когда просмотр загружает только добавить проверку, чтобы запустить его один раз или еще лучше добавить проверку в viewdidlayoutsubviews, чтобы запустить его однажды