Drake
Drake

Reputation: 2703

Convert Swift to C# to use in Xamarin

I am writing an Xamarin.iOS app, and found some Swift code for a loading spinner that I would like to use. So I am trying to convert the Swift code to C#. I hope this question is ok because I've been trying to convert this code for hours. This is the Swift code:

public class CircularProgressView: UIView {

public dynamic var progress: CGFloat = 0 {
    didSet {
        progressLayer.progress = progress
    }
}

fileprivate var progressLayer: CircularProgressLayer {
    return layer as! CircularProgressLayer
}

override public class var layerClass: AnyClass {
    return CircularProgressLayer.self
}


override public func action(for layer: CALayer, forKey event: String) -> CAAction? {
    if event == #keyPath(CircularProgressLayer.progress),
        let action = action(for: layer, forKey: #keyPath(backgroundColor)) as? CAAnimation {

        let animation = CABasicAnimation()
        animation.keyPath = #keyPath(CircularProgressLayer.progress)
        animation.fromValue = progressLayer.progress
        animation.toValue = progress
        animation.beginTime = action.beginTime
        animation.duration = action.duration
        animation.speed = action.speed
        animation.timeOffset = action.timeOffset
        animation.repeatCount = action.repeatCount
        animation.repeatDuration = action.repeatDuration
        animation.autoreverses = action.autoreverses
        animation.fillMode = action.fillMode
        animation.timingFunction = action.timingFunction
        animation.delegate = action.delegate
        self.layer.add(animation, forKey: #keyPath(CircularProgressLayer.progress))
    }
    return super.action(for: layer, forKey: event)
}
}

fileprivate class CircularProgressLayer: CALayer {
@NSManaged var progress: CGFloat
let startAngle: CGFloat = 1.5 * .pi
let twoPi: CGFloat = 2 * .pi
let halfPi: CGFloat = .pi / 2


override class func needsDisplay(forKey key: String) -> Bool {
    if key == #keyPath(progress) {
        return true
    }
    return super.needsDisplay(forKey: key)
}

override func draw(in ctx: CGContext) {
    super.draw(in: ctx)

    UIGraphicsPushContext(ctx)

    //Light Grey
    UIColor.lightGray.setStroke()

    let center = CGPoint(x: bounds.midX, y: bounds.midY)
    let strokeWidth: CGFloat = 4
    let radius = (bounds.size.width / 2) - strokeWidth
    let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true)
    path.lineWidth = strokeWidth
    path.stroke()


    //Red
    UIColor.red.setStroke()

    let endAngle = (twoPi * progress) - halfPi
    let pathProgress = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle , clockwise: true)
    pathProgress.lineWidth = strokeWidth
    pathProgress.lineCapStyle = .round
    pathProgress.stroke()

    UIGraphicsPopContext()
}
}

let circularProgress = CircularProgressView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
UIView.animate(withDuration: 2, delay: 0, options: .curveEaseInOut, animations: {
circularProgress.progress = 0.76
}, completion: nil)

This is what I have so far:

public class CircularProgressView : UIView
{
    // I doubt I made these properties correctly?

    public float Progress
    {
        get
        {
            return ProgressLayer.Progress;
        }
        set
        {
            ProgressLayer.Progress = value;
        }
    }

    public CircularProgressLayer ProgressLayer { get; set; }

    public override Foundation.NSObject ActionForLayer(CALayer layer, string eventKey)
    {
        // I don't understand this part

        return base.ActionForLayer(layer, eventKey);
    }
}

public class CircularProgressLayer : CALayer
{
    public float Progress { get; set; }
    private nfloat startAngle = (nfloat)(1.5f * Math.PI);
    private nfloat twoPi = (nfloat)(2 * Math.PI);
    private double halfPi = Math.PI / 2;

    public override bool NeedsDisplay
    {
        get
        {
                return base.NeedsDisplay;
        }
    }

    public override void DrawInContext(CoreGraphics.CGContext ctx)
    {
        base.DrawInContext(ctx);

        UIGraphics.PushContext(ctx);

        UIColor.LightGray.SetStroke();

        var center = new CGPoint(x: Bounds.GetMidX(), y: Bounds.GetMidY());
        float strokeWidth = 4;
        nfloat radius = (Bounds.Size.Width / 2) - strokeWidth;
        var path = UIBezierPath.FromArc(center, radius: radius, startAngle: 0, endAngle: twoPi, clockwise: true);
        path.LineWidth = strokeWidth;
        path.Stroke();

        UIColor.Red.SetStroke();

        nfloat endAngle = (nfloat)((twoPi * Progress) - halfPi);
        var pathProgress = UIBezierPath.FromArc(center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true);
        pathProgress.LineWidth = strokeWidth;
        pathProgress.LineCapStyle = CGLineCap.Round;
        pathProgress.Stroke();

        UIGraphics.PopContext();
    }
}

There's a few parts that I do not understand, like this one:

if event == #keyPath(CircularProgressLayer.progress)

and this one:

fileprivate var progressLayer: CircularProgressLayer {
    return layer as! CircularProgressLayer
}

I'm very good at C# but am new to Swift. I hope this question is ok because I've been trying to convert this code for hours.

Upvotes: 3

Views: 5019

Answers (2)

SushiHangover
SushiHangover

Reputation: 74164

Update:

I needed to create @dynamic / NSManaged in a Xamarin.iOS prototype and ended up being able to do this by creating them at runtime, see my comment here:

And related gist:

So while Xamarin.iOS does not support them out-of-the-box, you can create them at runtime if needed.

Original:

Currently you can not generate dynamic properties (@dynamic / NSManaged) using Xamarin.iOS so the code you posted will not work even if converted.

Issue: https://bugzilla.xamarin.com/show_bug.cgi?id=38823

If manually constructing CAAction/CAAnimation objects is a valid option for your needs, you can review the Xamarin.iOS CustomPropertyAnimation example app:

Upvotes: 2

Kamil Szostakowski
Kamil Szostakowski

Reputation: 2163

This is the compiler friendly way for expressing key paths in Swift. Keypath is a series of properties in the consecutive objects (eg. company.manager.fullName). If you want to learn more, read about the KVC. This expression produces a String, in your case, it will be "progress". I think you can replace it with a simple comparison.

#keyPath(CircularProgressLayer.progress)

This is the read-only property which exposes the layer property as an instance of the CircularProgressLayer. The x as! y means: cast x to y or crash if not possible.

fileprivate var progressLayer: CircularProgressLayer {
    return layer as! CircularProgressLayer
}

Upvotes: 1

Related Questions